简体   繁体   中英

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("}")

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

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

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.

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)

You may take the addString function of the TraversableOnce trait 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 }

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)
  }
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM