梁嘉腾
梁嘉腾

Reputation: 11

Scala advanced loop

  var z = 10
  for (i <- 1 to 3; x = i + 1; y = z - 1) {
    println(x)
    println(y)
    z -= 1
  }

Output:

2 9 3 9 4 9

It is not what I expect. It seems the for loop always keeps z = 10. Can someone help me explain this?

Upvotes: 0

Views: 254

Answers (2)

Andrey Tyukin
Andrey Tyukin

Reputation: 44908

Scala's for are not imperative loops, they are general monadic comprehensions. Every for and for-yield is rewritten into a sequence of applications of methods foreach, map, flatMap and withFilter.

For example, this here:

for (x <- generator) yield { f(x) }

is rewritten into

generator.map(x => f(x))

If you use definitions like x = ... inside of for-comprehensions, they are rewritten as follows:

for {
  g <- generator
  x = xRhs
} {
  doBlah()
}

is rewritten to

for {
  (g, x) <- for (g <- generator) yield (g, xRhs)
} {
  doBlah()
}

and then to

(for (g <- generator) yield (g,xRhs)).foreach{ 
  case (g, x) => doBlah() 
}

and finally to

generator.map(g => (g, xRhs)).foreach{ case (g, x) => doBlah() }

For your example it means the following horrific rewriting sequence (copy-paste it, run it, try to follow each rewriting step):

def ----- : Unit = println("-" * 40)

{
  var z = 10
  for (i <- 1 to 3; x = i + 1; y = z - 1) {
    // println(s"i = $i , x = $x , y = $y, z = $z")
    println(x)
    println(y)
    z -= 1
  }
}

-----

{
  var z = 10
  for {
    (i, x) <- for (i <- 1 to 3) yield (i, i + 1)
    y = z - 1
  } {
    println(x)
    println(y)
    z -= 1 
  }
}

-----

{
  var z = 10
  for {
    ((i, x), y) <- for ((i, x) <- for (i <- 1 to 3) yield (i, i + 1)) yield ((i, x), z - 1)
  } {
    println(x)
    println(y)
    z -= 1 
  }
}

-----

{
  var z = 10
  for {
    ((i, x), y) <- for ((i, x) <- (1 to 3).map(i => (i, i + 1))) yield ((i, x), z - 1)
  } {
    println(x)
    println(y)
    z -= 1
  }
}

-----

{
  var z = 10
  for {
    ((i, x), y) <- (1 to 3).map(i => (i, i + 1)).map(ix => (ix, z - 1))
  } {
    println(x)
    println(y)
    z -= 1
  }
}

-----

{
  var z = 10
  (1 to 3).map(i => (i, i + 1)).map(ix => (ix, z - 1)).foreach {
    case ((i, x), y) => 
      println(x)
      println(y)
      z -= 1
  }
}

The final result is (morally equivalent to) something like this:

  var z = 10
  (1 to 3).map(i => (i, i + 1)).map(ix => (ix, z - 1)).foreach {
    case ((i, x), y) => 
      println(x)
      println(y)
      z -= 1
  }

As you can see, the z - 1 in the second map is evaluated even before the foreach-iteration starts. Therefore y remains stuck at 9.

How to avoid it? Just avoid using vars in combination with for-comprehensions. Better: use vars sparingly in general. Or at least, move them inside the block:

  var z = 10
  for (i <- 1 to 3) {
    val x = i + 1
    val y = z - 1
    println(x)
    println(y)
    z -= 1
  }

Upvotes: 2

Jason Hu
Jason Hu

Reputation: 6333

You can inspect what you are running as following:

scala> import scala.reflect.runtime.{universe => ru}
scala> import ru._
scala> q"""for (i <- 1 to 3; x = i + 1; y = z - 1) {
     |     println(x)
     |     println(y)
     |     z -= 1
     |   }
     | """
res13: reflect.runtime.universe.Tree =
1.to(3).map(((i) => {
  val x = i.$plus(1);
  val y = z.$minus(1);
  scala.Tuple3(i, x, y)
})).foreach(((x$1) => x$1: @scala.unchecked match {
  case scala.Tuple3((i @ _), (x @ _), (y @ _)) => {
    println(x);
    println(y);
    z.$minus$eq(1)
  }
}))

If you see the expanded code, I believe it should be enough to convince you that y should always be 9.

Upvotes: 2

Related Questions