Search code examples
scalafor-comprehension

What are the scoping rules for vals in Scala for-comprehensions


When I use a val in a for-comprehension, I get the warning:

warning: val keyword in for comprehension is deprecated

despite the production in the syntax appendix of the spec.

This suggests that when I do something like

for (x <- xs; a = x)

I'm not really introducing a variable, such as if I do something like

for (x <- xs) yield { implicit val a = x; /* more */ }

where, as usual, the brace starts a new scope where I can introduce a new val, or even a new implicit.

What am I really doing with that a?

Am I consuming stack space? Heap? Some other kind of alias?


Solution

  • Like an ordinary val pat = expr definition, the thing to the left of the equals sign is just a pattern.

    The Enumerator production in the syntax spec shows that the clause in a for-expr can be a generator (a <- b), guard if cond or val def a = b.

    The parts that can be arbitrary expressions are b (as given to the right of <- and =) and the condition.

    Responder.exec takes advantage of the conditional to execute arbitrary code, while evaluating trivially to true.

    That means you could do arbitrary side-effects from a conditional:

    // yucky, yet instructive
    scala> val xs = List(1,2,3)
    scala> def bar(implicit i: Int) = Some(i+1)
    scala> implicit var imp: Int = 0
    scala> for { a<-xs; if { imp=a; true }; b<-bar } yield b
    res6: List[Int] = List(2, 3, 4)
    

    Similarly, the val def desugars as follows:

    tmp <- xs
    a = f(tmp)  // some arbitrary function of tmp
    // amounts to
    (tmp, a) <- for (x@tmp <- xs) yield { val x0@a=f(tmp); (x, x0) }
    

    Wait, really?

    scala> def f(vs: List[Int]) = for (a <- vs; b = a+1) yield b
    f: (vs: List[Int])List[Int]
    

    You'll need a recent repl to do this:

    scala> :javap f
    [snip]
      public scala.collection.immutable.List<java.lang.Object> f(scala.collection.immutable.List<java.lang.Object>);
        flags: ACC_PUBLIC
    
        Code:
          stack=3, locals=2, args_size=2
             0: aload_1       
             1: new           #16                 // class $anonfun$f$1
             4: dup           
             5: invokespecial #17                 // Method $anonfun$f$1."<init>":()V
             8: getstatic     #22                 // Field scala/collection/immutable/List$.MODULE$:Lscala/collection/immutable/List$;
            11: invokevirtual #26                 // Method scala/collection/immutable/List$.canBuildFrom:()Lscala/collection/generic/CanBuildFrom;
            14: invokeinterface #32,  3           // InterfaceMethod scala/collection/TraversableLike.map:(Lscala/Function1;Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;
            19: checkcast     #28                 // class scala/collection/TraversableLike
            22: new           #34                 // class $anonfun$f$2
            25: dup           
            26: invokespecial #35                 // Method $anonfun$f$2."<init>":()V
            29: getstatic     #22                 // Field scala/collection/immutable/List$.MODULE$:Lscala/collection/immutable/List$;
            32: invokevirtual #26                 // Method scala/collection/immutable/List$.canBuildFrom:()Lscala/collection/generic/CanBuildFrom;
            35: invokeinterface #32,  3           // InterfaceMethod scala/collection/TraversableLike.map:(Lscala/Function1;Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;
            40: checkcast     #37                 // class scala/collection/immutable/List
            43: areturn       
    

    I see two invocations of map, for the intermediate expression and for the yield.

    On further inspection, the first anonfun is not a Int => Int (i.e., a+1) but a Int => (Int,Int).

    So the val we introduced is just getting passed around as part of a tuple.