deltanovember
deltanovember

Reputation: 44051

Is there a way to handle the last case differently in a Scala for loop?

For example suppose I have

  for (line <- myData) {
    println("}, {")
  }

Is there a way to get the last line to print

println("}")

Upvotes: 18

Views: 5093

Answers (6)

Sebastian N.
Sebastian N.

Reputation: 1991

Other answers are rightfully pointed to mkString, and for a normal amount of data I would also use that.

However, mkString builds (accumulates) the end-result in-memory through a StringBuilder. This is not always desirable, depending on the amount of data we have.

In this case, if all we want is to "print" we don't need to build the big-result first (and maybe we even want to avoid this).

Consider the implementation of this helper function:

def forEachIsLast[A](iterator: Iterator[A])(operation: (A, Boolean) => Unit): Unit = {
  while(iterator.hasNext) {
    val element = iterator.next()
    val isLast = !iterator.hasNext // if there is no "next", this is the last one
    operation(element, isLast)
  }
}    

It iterates over all elements and invokes operation passing each element in turn, with a boolean value. The value is true if the element passed is the last one.

In your case it could be used like this:

forEachIsLast(myData) { (line, isLast) =>
  if(isLast)
    println("}")
  else
    println("}, {")
}

We have the following advantages here:

  • It operates on each element, one by one, without necessarily accumulating the result in memory (unless you want to).
  • Because it does not need to load the whole collection into memory to check its size, it's enough to ask the Iterator if it's exhausted or not. You could read data from a big file, or from the network, etc.

Upvotes: 2

4e6
4e6

Reputation: 10776

If you don't want to use built-in mkString function, you can make something like

for (line <- lines)
  if (line == lines.last) println("last")
  else println(line)

UPDATE: As didierd mentioned in comments, this solution is wrong because last value can occurs several times, he provides better solution in his answer.

It is fine for Vectors, because last function takes "effectively constant time" for them, as for Lists, it takes linear time, so you can use pattern matching

@tailrec
def printLines[A](l: List[A]) {
  l match {
    case Nil => 
    case x :: Nil => println("last")
    case x :: xs => println(x); printLines(xs)
  }
}

Upvotes: 3

Didier Dupont
Didier Dupont

Reputation: 29528

I agree fully with what has been said before about using mkstring, and distinguishing the first iteration rather than the last one. Would you still need to distinguish on the last, scala collections have an init method, which return all elements but the last. So you can do

for(x <- coll.init) workOnNonLast(x)
workOnLast(coll.last)

(init and last being sort of the opposite of head and tail, which are the first and and all but first). Note however than depending on the structure, they may be costly. On Vector, all of them are fast. On List, while head and tail are basically free, init and last are both linear in the length of the list. headOption and lastOption may help you when the collection may be empty, replacing workOnlast by

for (x <- coll.lastOption) workOnLast(x)

Upvotes: 15

Kevin Wright
Kevin Wright

Reputation: 49705

Before going any further, I'd recommend you avoid println in a for-comprehension. It can sometimes be useful for tracking down a bug that occurs in the middle of a collection, but otherwise leads to code that's harder to refactor and test.

More generally, life usually becomes easier if you can restrict where any sort of side-effect occurs. So instead of:

for (line <- myData) {
  println("}, {")
}

You can write:

val lines = for (line <- myData) yield "}, {"
println(lines mkString "\n")

I'm also going to take a guess here that you wanted the content of each line in the output!

val lines = for (line <- myData) yield (line + "}, {")
println(lines mkString "\n")

Though you'd be better off still if you just used mkString directly - that's what it's for!

val lines = myData.mkString("{", "\n}, {", "}")
println(lines)

Note how we're first producing a String, then printing it in a single operation. This approach can easily be split into separate methods and used to implement toString on your class, or to inspect the generated String in tests.

Upvotes: 21

Tomasz Nurkiewicz
Tomasz Nurkiewicz

Reputation: 340753

Can you refactor your code to take advantage of built-in mkString?

scala> List(1, 2, 3).mkString("{", "}, {", "}")
res1: String = {1}, {2}, {3}

Upvotes: 29

Nicolas
Nicolas

Reputation: 24769

You may take the addString function of the TraversableOncetrait as an example.

def addString(b: StringBuilder, start: String, sep: String, end: String): StringBuilder = {
  var first = true

  b append start   
  for (x <- self) {
    if (first) {
      b append x
      first = false
    } else {
      b append sep
      b append x
    }
  }
  b append end

  b
}

In your case, the separator is }, { and the end is }

Upvotes: 6

Related Questions