Feature request: supplement by-name parameter syntax with "lazy" keyword

I can define a lazy value like so:

lazy val i = { print("evaluated!"); 42 }

If I pass that value to a method as a normal parameter then it will be immediately evaluated:

def willEvaluate(n: Int) = println("hello")
willEvaluate(i) // Prints "evaluated! hello"

I can defer evaluation by passing it as a by-name parameter

def wontEvaluate(n: => Int) = println("hello")
wontEvaluate(i) // Prints "hello"

Given the similar nature of by-name parameters and lazy values it is odd that you can’t use the lazy keyword as a method of passing lazy values without evaluating them:

def doesntCompile(lazy val n: Int) = ...
def doesntCompile(lazy n: Int) = ...
def doesntCompile(n: lazy Int) = ...

In fact, when one passes a lazy val: Int where a => Int is required one might actually be surprised that it does compile, as at first glance the types seem to be different. When it does compile, you may think, “What is happening? Is there an implicit conversion? Is one syntactic sugar for the other?”

I submit that allowing one to define a lazy parameter would improve Scala’s readability and useability.

Are there important distinctions between => Int and lazy val i: Int that would preempt such syntax?

2 Likes

Lazy evaluates only once. Call by name evaluates each time it is needed.

I like the idea, but I think it should work like lazy and not call by name.

2 Likes

This just reminds me that ‘lazy val’ is a bad name and leads to poor assumptions about how best to use it. A better name would have been ‘memoize def’.

After all, that is what it is. It behaves like a def more than a val in many critical ways (especially w.r.t. initialization and inheritance).

I frequently need to teach beginners this, telling them not to think of it as a val at all.

The lazy prefix is only applicable in contexts where val can be replaced with def… because its truly a def at heart.

1 Like

Actually it’s not. Memoization can be updated if the input is updated. Lazy vals are only updated once.

I think it should work like lazy and not call by name.

Yes! That would be even better.

Here is a request for exactly this in 2007: https://github.com/scala/bug/issues/240

Only in the case that def and val can be interchanged (no params, a member or local declaration) can ‘lazy’ be prepended to val.

There are no parameters here. Its isomorphic to memoizing a parameterless def.

One could say that Scala only supports memoization of parameterless defs, and extend that to memoizing by parameters later. But ‘lazy vals with parameters’ doesnt make sense.

OK, I understand what you mean now.

Maybe you should checkout this dotty PR: https://github.com/lampepfl/dotty/pull/6967

Semantically it is a val in that (1) it requires storage for its contents, and (2) it doesn’t change.

Semantically it is lazy in that it isn’t computed until it’s needed.

Memoization of parameterless* immutable/pure defs is maybe semantically equivalent, but why explain a simple two-part concept with four concepts, one of which (memoization) is not exactly simple, and which doesn’t tell the entire story unless you think carefully about implementation?

(* They’re generally not actually parameterless unless they’re useless. The parameters are just passed implicitly e.g. by being part of the object upon which the def is defined. So I don’t know that this concept is simple either.)

4 Likes

See the discussion on the Dotty issue here.

TL;DR the main gain here would be the ability use lazy parameters (given or normal) in stable paths. However, there are open issues (null-related) about soundness of lazy vals in stable paths.

2 Likes

I think that actually nothing unusual happens, only the intuition is wrong. => A is a sugar for () => A (which itself is also a sugar). => A is shorter than () => A and also allows to change from call-by-value to call-by-name without changing client code.

If you have a method def method1(param1: => Int): Unit and you call it as method1(3 + compute("x", 2)) then it’s equivalent as having def method2(param1: () => Int): Unit and calling it as method2(() => 3 + compute("x", 2)). The argument body is not special and you can of course use lazy val in it. What happens then? method1(someLazyVal + 5) is eqivalent to method2(() => someLazyVal + 5) so if someLazyVal is not evaluated before method2 invocation then only invoking the passed in function evaluated that lazy val.

Here’s a sample code https://scastie.scala-lang.org/Rrwv1gPSRra94ZhYh6Ve3Q that illustrates that call-by-name and explicit function variants are equivalent:

def methodByName(param: => Int): Unit = {
  println("entered methodByName")
	param + param
}

def methodFunction(param: () => Int): Unit = {
  println("entered methodFunction")
	param() + param()
}

println("step 1a")
lazy val v1a = { println("v1a"); 1 }
methodByName(v1a)
println('.')

println("step 1b")
lazy val v1b = { println("v1b"); 1 }
methodFunction(() => v1b)
println('.')

println("step 2a")
lazy val v2a = { println("v2a"); 1 }
methodByName({ println("a2a"); 1 } + v2a)
println('.')

println("step 2b")
lazy val v2b = { println("v2b"); 1 }
methodFunction(() => { println("a2b"); 1 } + v2b)
println('.')

Output is:

step 1a
entered methodByName
v1a
.
step 1b
entered methodFunction
v1b
.
step 2a
entered methodByName
a2a
v2a
a2a
.
step 2b
entered methodFunction
a2b
v2b
a2b
.

If you change lazy vals to ordinary vals or if you change lazy vals to defs then you will still get exactly the same behaviour in A and B variants. In fact both variants looks the same in bytecode, here’s the output of javap -p -c

Compiled from "Scala.scala"
public final class temp.Scala$ {
  public static temp.Scala$ MODULE$;

  public static {};
    Code:
       0: new           #2                  // class temp/Scala$
       3: invokespecial #17                 // Method "<init>":()V
       6: return

  // here we see that methodByName and methodFunction compile to exactly the same bytecode (modulo method name of course)
  public void methodByName(scala.Function0<java.lang.Object>);
    Code:
       0: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: ldc           #28                 // String entered methodByName
       5: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
       8: aload_1
       9: invokeinterface #38,  1           // InterfaceMethod scala/Function0.apply$mcI$sp:()I
      14: aload_1
      15: invokeinterface #38,  1           // InterfaceMethod scala/Function0.apply$mcI$sp:()I
      20: iadd
      21: pop
      22: return

  public void methodFunction(scala.Function0<java.lang.Object>);
    Code:
       0: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: ldc           #43                 // String entered methodFunction
       5: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
       8: aload_1
       9: invokeinterface #38,  1           // InterfaceMethod scala/Function0.apply$mcI$sp:()I
      14: aload_1
      15: invokeinterface #38,  1           // InterfaceMethod scala/Function0.apply$mcI$sp:()I
      20: iadd
      21: pop
      22: return

  public void main(java.lang.String[]);
    Code:
      // priming lazy val slots
       0: new           #48                 // class scala/runtime/LazyInt
       3: dup
       4: invokespecial #49                 // Method scala/runtime/LazyInt."<init>":()V
       7: astore_2
       8: new           #48                 // class scala/runtime/LazyInt
      11: dup
      12: invokespecial #49                 // Method scala/runtime/LazyInt."<init>":()V
      15: astore_3
      16: new           #48                 // class scala/runtime/LazyInt
      19: dup
      20: invokespecial #49                 // Method scala/runtime/LazyInt."<init>":()V
      23: astore        4
      25: new           #48                 // class scala/runtime/LazyInt
      28: dup
      29: invokespecial #49                 // Method scala/runtime/LazyInt."<init>":()V
      32: astore        5
      // step 1a
      34: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      37: ldc           #51                 // String step 1a
      39: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      42: aload_0
      43: aload_2
      44: invokedynamic #71,  0             // InvokeDynamic #0:apply$mcI$sp:(Lscala/runtime/LazyInt;)Lscala/runtime/java8/JFunction0$mcI$sp;
      49: invokevirtual #73                 // Method methodByName:(Lscala/Function0;)V
      52: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      55: bipush        46
      57: invokestatic  #79                 // Method scala/runtime/BoxesRunTime.boxToCharacter:(C)Ljava/lang/Character;
      60: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      // step 1b - notice how similar it is to step 1a (exactly the same bytecodes)
      63: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      66: ldc           #81                 // String step 1b
      68: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      71: aload_0
      72: aload_3
      73: invokedynamic #86,  0             // InvokeDynamic #1:apply$mcI$sp:(Lscala/runtime/LazyInt;)Lscala/runtime/java8/JFunction0$mcI$sp;
      78: invokevirtual #88                 // Method methodFunction:(Lscala/Function0;)V
      81: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      84: bipush        46
      86: invokestatic  #79                 // Method scala/runtime/BoxesRunTime.boxToCharacter:(C)Ljava/lang/Character;
      89: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      // step 2a
      92: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      95: ldc           #90                 // String step 2a
      97: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
     100: aload_0
     101: aload         4
     103: invokedynamic #95,  0             // InvokeDynamic #2:apply$mcI$sp:(Lscala/runtime/LazyInt;)Lscala/runtime/java8/JFunction0$mcI$sp;
     108: invokevirtual #73                 // Method methodByName:(Lscala/Function0;)V
     111: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
     114: bipush        46
     116: invokestatic  #79                 // Method scala/runtime/BoxesRunTime.boxToCharacter:(C)Ljava/lang/Character;
     119: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      // step 2b - notice how similar it is to step 2a (exactly the same bytecodes)
     122: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
     125: ldc           #97                 // String step 2b
     127: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
     130: aload_0
     131: aload         5
     133: invokedynamic #102,  0            // InvokeDynamic #3:apply$mcI$sp:(Lscala/runtime/LazyInt;)Lscala/runtime/java8/JFunction0$mcI$sp;
     138: invokevirtual #88                 // Method methodFunction:(Lscala/Function0;)V
     141: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
     144: bipush        46
     146: invokestatic  #79                 // Method scala/runtime/BoxesRunTime.boxToCharacter:(C)Ljava/lang/Character;
     149: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
     152: return

  private static final int v1a$lzycompute$1(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: aload_0
       5: invokevirtual #114                // Method scala/runtime/LazyInt.initialized:()Z
       8: ifeq          18
      11: aload_0
      12: invokevirtual #117                // Method scala/runtime/LazyInt.value:()I
      15: goto          31
      18: aload_0
      19: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      22: ldc           #119                // String v1a
      24: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      27: iconst_1
      28: invokevirtual #123                // Method scala/runtime/LazyInt.initialize:(I)I
      31: istore_2
      32: aload_1
      33: monitorexit
      34: iload_2
      35: goto          41
      38: aload_1
      39: monitorexit
      40: athrow
      41: ireturn
    Exception table:
       from    to  target type
           4    32    38   any

  private static final int v1a$1(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: invokevirtual #114                // Method scala/runtime/LazyInt.initialized:()Z
       4: ifeq          14
       7: aload_0
       8: invokevirtual #117                // Method scala/runtime/LazyInt.value:()I
      11: goto          18
      14: aload_0
      15: invokestatic  #128                // Method v1a$lzycompute$1:(Lscala/runtime/LazyInt;)I
      18: ireturn

  public static final int $anonfun$main$1(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: invokestatic  #130                // Method v1a$1:(Lscala/runtime/LazyInt;)I
       4: ireturn

  private static final int v1b$lzycompute$1(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: aload_0
       5: invokevirtual #114                // Method scala/runtime/LazyInt.initialized:()Z
       8: ifeq          18
      11: aload_0
      12: invokevirtual #117                // Method scala/runtime/LazyInt.value:()I
      15: goto          31
      18: aload_0
      19: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      22: ldc           #134                // String v1b
      24: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      27: iconst_1
      28: invokevirtual #123                // Method scala/runtime/LazyInt.initialize:(I)I
      31: istore_2
      32: aload_1
      33: monitorexit
      34: iload_2
      35: goto          41
      38: aload_1
      39: monitorexit
      40: athrow
      41: ireturn
    Exception table:
       from    to  target type
           4    32    38   any

  private static final int v1b$1(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: invokevirtual #114                // Method scala/runtime/LazyInt.initialized:()Z
       4: ifeq          14
       7: aload_0
       8: invokevirtual #117                // Method scala/runtime/LazyInt.value:()I
      11: goto          18
      14: aload_0
      15: invokestatic  #137                // Method v1b$lzycompute$1:(Lscala/runtime/LazyInt;)I
      18: ireturn

  public static final int $anonfun$main$2(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: invokestatic  #139                // Method v1b$1:(Lscala/runtime/LazyInt;)I
       4: ireturn

  private static final int v2a$lzycompute$1(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: aload_0
       5: invokevirtual #114                // Method scala/runtime/LazyInt.initialized:()Z
       8: ifeq          18
      11: aload_0
      12: invokevirtual #117                // Method scala/runtime/LazyInt.value:()I
      15: goto          31
      18: aload_0
      19: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      22: ldc           #143                // String v2a
      24: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      27: iconst_1
      28: invokevirtual #123                // Method scala/runtime/LazyInt.initialize:(I)I
      31: istore_2
      32: aload_1
      33: monitorexit
      34: iload_2
      35: goto          41
      38: aload_1
      39: monitorexit
      40: athrow
      41: ireturn
    Exception table:
       from    to  target type
           4    32    38   any

  private static final int v2a$1(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: invokevirtual #114                // Method scala/runtime/LazyInt.initialized:()Z
       4: ifeq          14
       7: aload_0
       8: invokevirtual #117                // Method scala/runtime/LazyInt.value:()I
      11: goto          18
      14: aload_0
      15: invokestatic  #146                // Method v2a$lzycompute$1:(Lscala/runtime/LazyInt;)I
      18: ireturn

  public static final int $anonfun$main$3(scala.runtime.LazyInt);
    Code:
       0: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: ldc           #148                // String a2a
       5: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
       8: iconst_1
       9: aload_0
      10: invokestatic  #150                // Method v2a$1:(Lscala/runtime/LazyInt;)I
      13: iadd
      14: ireturn

  private static final int v2b$lzycompute$1(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: aload_0
       5: invokevirtual #114                // Method scala/runtime/LazyInt.initialized:()Z
       8: ifeq          18
      11: aload_0
      12: invokevirtual #117                // Method scala/runtime/LazyInt.value:()I
      15: goto          31
      18: aload_0
      19: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      22: ldc           #154                // String v2b
      24: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      27: iconst_1
      28: invokevirtual #123                // Method scala/runtime/LazyInt.initialize:(I)I
      31: istore_2
      32: aload_1
      33: monitorexit
      34: iload_2
      35: goto          41
      38: aload_1
      39: monitorexit
      40: athrow
      41: ireturn
    Exception table:
       from    to  target type
           4    32    38   any

  private static final int v2b$1(scala.runtime.LazyInt);
    Code:
       0: aload_0
       1: invokevirtual #114                // Method scala/runtime/LazyInt.initialized:()Z
       4: ifeq          14
       7: aload_0
       8: invokevirtual #117                // Method scala/runtime/LazyInt.value:()I
      11: goto          18
      14: aload_0
      15: invokestatic  #157                // Method v2b$lzycompute$1:(Lscala/runtime/LazyInt;)I
      18: ireturn

  public static final int $anonfun$main$4(scala.runtime.LazyInt);
    Code:
       0: getstatic     #26                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: ldc           #159                // String a2b
       5: invokevirtual #32                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
       8: iconst_1
       9: aload_0
      10: invokestatic  #161                // Method v2b$1:(Lscala/runtime/LazyInt;)I
      13: iadd
      14: ireturn

  private temp.Scala$();
    Code:
       0: aload_0
       1: invokespecial #162                // Method java/lang/Object."<init>":()V
       4: aload_0
       5: putstatic     #164                // Field MODULE$:Ltemp/Scala$;
       8: return

  private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);
    Code:
       0: aload_0
       1: invokedynamic #176,  0            // InvokeDynamic #4:lambdaDeserialize:(Ljava/lang/invoke/SerializedLambda;)Ljava/lang/Object;
       6: areturn
}

Notice how both => Int and () => Int got both compiled down to Function0[Int] and how bytecodes for both variants (A with call-by-name and B with explicit Function0) are exactly the same.

TL;DR:
lazy val was not converted to Function0 when passing argument by name, it was wrapped in it just as anything else would.

2 Likes