This thread is motivated by a recent discussion in the gitter channel; I recommend taking a look at the discussion for having full context but I will try to provide a summary.
The TL;DR; is that the fact that by-name parameters have their own type is confusing for many users (as can be seen in the gitter discussion, and folks may recall different but related discussions over the years). Most people would expect that the following method signature:
def foo(bar: => Bar): Baz would be eta-expanded into just
Bar => Baz, but it actually expands into
(=> Bar) => Baz, which is not only a different type than expected but also a weird one, especially because it seems is not possible to define a value of such type using the lambda syntax; and additionally, it is not clear (or at least not for me) if
=> Baz alone is a valid type or not.
The idea of this thread is to propose an alternative encoding of by-name parameters that emerged from the gitter discussion. The proposal attempts to make by-name parameters less weird but retaining their utility; although this proposal alone will not address the previous issue (more on that later).
First, let’s start with three assumptions:
- The utility of by-name parameters is its usage syntax which gives the impression that we are using a control structure defined in the language. Of course, this alone is useless if it wouldn’t be for the laziness of the argument, however, we may just ask for a
() => Ato have that.
- They are actually implemented as a
Function0value under the hood.
- Library authors won’t have much problem applying the necessary refactor, especially if the appropriate Scalafix rules and cross-compilation guarantees are given.
With all that the idea is very simple. An argument of type
=> A would be seen as just a
() => A in the body of the method, the only difference will be that at the usage site user will omit the
() => and will just provide the body of the function (making the usage syntax exactly the same as of today). Similar as to how varargs are implemented, where an argument of type
A* is just a
Seq[A] with sugar syntax at the call site.
Of course, similar to
: _* for varargs, we may need to provide something like
: => to manually lift a
() => A value into a
=> A argument instead of producing a
() => (() => A).
The advantage of this is making the feature more simple and predictable. The disadvantage is it would be a source breaking change since now one would need to manually execute the function to get its value; e.g
thunk() instead of just
thunk. However, I think this is actually a good thing since it makes them safer to pass around.
Now, regarding the original issue of eta-expanding a
def foo (bar: => Bar): Baz into a
Bar => Baz, while this proposal doesn’t do that (since it now will eta-expand into a
(() => Bar) => Baz) I think this is a better situation because the type is a regular one and the error would be clearer and also most people would already know that is what was going to happen.
Nevertheless, it may be possible to just implement an implicit conversion or an extension method to easily go from
(() => A) => B into
A => B; such conversion / extension method may even be part of the stdlib if maintainers agree is useful enough.
Please let me know what you all think about this.