SIP Proposal: Use parenthesis for call-by-name

By-name parameters do not automatically convert Function0[A] to A. By-name parameters have type => A, not () => A. There are other things that have type => A and therefore are evaluated simply because their name is referenced: defs!

def foo: Int = { println("hello"); 42 }
foo
foo

prints "hello" twice.

I do not believe in a proposal that would try to get rid of by-name parameters. They are well integrated in the language and widely used. Removing them would a lot of code.

4 Likes

@sjrd I need a better reason to not be rid of by-name parameters than it’s popular.

For me, what you’ve motivated is getting rid of parameter-less defs, for the same reasons as getting rid of by-name parameters.

Interestingly, even Scala doesn’t convert your foo into a => Int when using it as a function value. It converts it to a () => Int.

scala> def foo: Int = { println("hello"); 42 }
foo: Int

scala> foo _
res0: () => Int = $$Lambda$1051/1419428140@188a5fc2

If you do that you need to get rid of getters for vals and vars, which means losing the Uniform Access Principle. Down that road, 0% of the current Scala ecosystem still compiles. This is not realistic.

2 Likes

Conversely: I need a better reason to break the Scala ecosystem than it’s confusing for the author of a method using a by-name parameter.

2 Likes

Um – no, really, that’s not true. You’re talking about breaking vast amounts of existing code. That requires a stronger argument than maintaining the status quo does. While I understand your complaint, so far I’m unconvinced that it’s worth the headache


1 Like

If compatibility is an issue here, we can think about using scalafix to port old code.

Any SIP is potentially going to break existing code. If that’s a problem, then you’re really arguing against SIPs in general, not this particular one. What are the technical reasons this particular SIP bad? Does it reduce readability? Create undue stress for developers? In my opinion, those are better reasons a SIP is bad than “it breaks code.”

@sjrd Please give more details about how getting rid of parameterless defs means we “need to get rid of getters for vals and vars.”

Sorry if I missed it, but if we get rid of by-name, what would replace
expressions like Try { “Hello” } and Future { “World” }?

1 Like

When you declare a val in a class, you implicitly receive a parameter-less def which is the getter for that val. For example,

class Foo {
  val x: Int = 42
}

is really

class Foo {
  private val _x: Int = 42
  def x: Int = _x
}

Among other things, this is what allows inherited (abstract) defs to be implemented/overridden by a val:

class Parent {
  def x: Int
}

class Child extends Parent {
  val x: Int = 42
}

This only makes sense if x is defined as a ()-free def, since it means that whether you have a c: Child or a p: Parent, you can do c.x or p.x, and not c.x() nor p.x().

It is also the same feature that enables the Uniform Access Principle in Scala. I can write, at call-site, c.x whether x is declared as val x: Int or def x: Int (which also means I can swap a val x: Int for a def x: Int and conversely, without breaking the users of my class). There is no difference at use site between a stored property and a computed property. The UAP is recognized as a Good Thing for object-oriented languages.

If you remove ()-free defs from Scala, you kill the Uniform Access Principle, and you make very awkward the val-implements-def pattern.

Additionally, there is a widespread convention in Scala that ()-full defs imply side effects, while ()-free defs are side-effect-free computed properties.

Finally, the distinction between ()-full and ()-free defs is essential for the interoperability between Scala.js and JavaScript: calling a ()-full def of a JS type is equivalent to calling a zero-argument in JavaScript, whereas accessing a ()-free def of a JS type is equivalent to reading a field/property in JavaScript. For that reason, removing ()-free defs would basically kill Scala.js.

For all the reasons above, this not something that could be scalafix’ed.

2 Likes

Regarding motivations,

  1. You’re not just making by-name parameters “semantically equivalent” to Function0, you’re just removing by-name parameters and allowing Function0 to be represented as => A instead of () => A. That’s just awkward; better to remove by-name parameter syntax entirely.

  2. You can already pass by-name parameters seamlessly.

class ByName {
  def foo(i: => Int): Int = i
  def bar(i: => Int) = foo(i)
}

gives

  public int foo(scala.Function0<java.lang.Object>);
    Code:
       0: aload_1
       1: invokeinterface #16,  1           // InterfaceMethod scala/Function0.apply$mcI$sp:()I
       6: ireturn

  public int bar(scala.Function0<java.lang.Object>);
    Code:
       0: aload_0
       1: aload_1
       2: invokevirtual #23                 // Method foo:(Lscala/Function0;)I
       5: ireturn

Note that the i is not evaluated and re-thunked. It just goes in directly.

  1. Accidentally calling expensive stuff is always a concern. But having it be inconvenient to avoid generating expensive stuff you don’t need is equally a concern. By-name parameters make it easy to generate stuff when you need it. Losing them isn’t a win, it’s a wash. You do have to pay attention to what you’re doing, but you do anyway.

@curoli
Try {() => "Hello" }
and
Future {() => "World"}

@sjrd

scala> val x = 3
x: Int = 3

scala> x _
res1: () => Int = $$Lambda$1064/698336642@305aaedf

TIL. Thanks @sjrd!

Finally, the distinction between ()-full and ()-free defs is essential for the interoperability between Scala.js and JavaScript: calling a ()-full def of a JS type is equivalent to calling a zero-argument in JavaScript, whereas accessing a ()-free def of a JS type is equivalent to reading a field/property in JavaScript. For that reason, removing ()-free defs would basically kill Scala.js.

I was still going to argue for this change, but if it would really make Scala.js impossible to continue, then I’ll drop it.

Has anyone considered the performance implications here? By name Parameters use a different substitution model and should in theory, be much faster then Function0 calls. I believed (although I Avenue tested this) that a function 0 call would create lambdas, whereas call by name won’t. I suspect by name parameters are also easier for the JIT to optimise. I could be wrong here, but I would like either byte code proof that this does not affect performance or at least some benchmarks.

Also, is there going to be any syntactical difference between Function0 and Function1[Unit]?

I still don’t see this working

By-name parameters are desugared into Function0s by the compiler. The byte code is literally the same.

See, this is part of my problem with by-name parameters. It’s part of the execution model, and its execution is represented by its type. However, the type has a limited representation in surface syntax. That’s what I’d really like to fix. I can’t speak for @pathikrit.

=> A is so bizarre, it has special rules in the parser. It’s a type that’s only allowed in specific conditions, and you can never actually create an instance. defs don’t count, because they are : A, not : => A. As far as I know, no other Scala type behaves like by-name parameters.

An alternative to getting rid of it would be to add some syntax for it. I’m not sure what it should be.

Following is how Scala currently works.

scala> def f(i: => Int): Int = i
f: (i: => Int)Int

What is this => Int? I can’t actually create a value of that type. Somehow, anything that is : Int unifies with : => Int. Bizarrely, @sjrd, you say by-name parameters are Function0s, but a Function0[A] doesn’t unify with => A, whereas A does. Interestingly, they could. My first thought is that => A needs to have some constructor. I don’t care what.

Suppose we call the constructor for a by-name value byname.

scala> def x: Int = 3
f: Int

scala> def f(i: => Int): Int = i
f: (i: => Int)Int

scala> val xByName: => Int = byname x
xByName: => Int

scala> f(byname x)
3

scala> f(xByName)
3

scala> f(x)
<console>:1: error: by-name parameter type required here
       f(x)
         ^

Or, since by-name parameters are Function0s, make () => be required for passing a by-name parameter.

scala> def x: Int = 3
f: Int

scala> def f(i: => Int): Int = i
f: (i: => Int)Int

scala> val xByName: => Int = () => x
xByName: => Int

scala> f(() => x)
3

scala> f(xByName)
3

scala> f(x)
<console>:1: error: by-name parameter type required here
       f(x)
         ^

This would allow Function0[A] to be a synonym for => A. Perhaps also the other way around.

@sjrd please correct me if I’m wrong, but I don’t think this breaks the Unified Access Principle.

There is also A*, the repeated parameter type. It has similar restrictions than => A (actually slightly stricter).

I did not say that. I said by-name parameters are compiled away into the same byte code as Function0s. At the language level, they are very different. If we apply your reasoning and consider that all the things that are compiled into the same bytecode must be equal at the language level, then local vals and vars are the same, and we should remove val from the language in favor of var.

It would, but then we’re back to square one, where by-name parameters require a different way to write the call sites, which defeats the very purpose of by-name parameters, and breaks the eco-system.

It doesn’t, nor did any of your previous suggestions, with the exception of your counter-argument from earlier that by my reasoning, ()-free defs should be removed from the language too.

1 Like

Ah, interesting. I didn’t know * was part of the type system.

scala> def f(a: Int*): Int = ???
f: (a: Int*)Int

scala> f _
res4: Seq[Int] => Int = $$Lambda$1088/603273695@b859355

I think my last idea could be made mostly backwards (source) compatible. You’d just need the following in scope:

implicit def valueToByName[A](a: A): => A = () => A

Then you could do,

scala> def f(i: => Int): Int = i
f: (i: => Int)Int

scala> f(3)
3

Then => A wouldn’t have a special place in the type system. You could opt-out of type-safe by-name values if you wished.

That doesn’t work.

When your valueToByName implicit kicks in, it eagerly evaluates its argument and stores it in a, which is then captured by the Function0 that you create. Ergo, your argument has become by value instead of by-name.

1 Like

Damn, good catch.

This post must be at least 20 characters.

(i: => Int)Int is a method type. => Int is only meaningful in the context of a method type, not in isolation; see SLS 3.3.1.

It’s normal in Scala that the universe of method types is richer than the universe of value types. By-name arguments is just one example. SĂ©bastien mentioned repeated parameters as another. Consider also methods with type parameters (“polymorphic method types” in SLS 3.3.2):

scala> def foo[A]: A = ???
foo: [A]=> A

I can define a method with the type [A]=> A, but I can’t actually create a value of type [A]=>A, I can only create values of that type for some particular A. In order to convert foo into a value, I have to decide what A is.

scala> def foo[A]: A = ???
foo: [A]=> A

scala> foo _
<console>:13: error: _ must follow method; cannot follow [A]=> A
       foo _
       ^

scala> foo[Int] _
res3: () => Int = $$Lambda$1087/1560244891@10d18696
4 Likes