Scala Wart: Weak eta-expansion
See original GitHub issueOpening this issue, as suggested by Martin, to provide a place to discuss the individual warts brought up in the blog post Warts of the Scala Programming Language and the possibility of mitigating/fixing them in Dotty (and perhaps later in Scala 2.x). These are based on Scala 2.x behavior, which I understand Dotty follows closely, apologies in advance if it has already been fixed
Scala maintains a distinction between “functions” and “methods”: in general, methods are things you call on an object, whereas functions are objects themselves. However, since they’re so similar (“things you can call”), it gives you a way to easily wrap a method in a function object called “eta expansion”
@ def repeat(s: String, i: Int) = s * i
defined function repeat
@ repeat("hello", 2)
res89: String = "hellohello"
@ val func = repeat _
func: (String, Int) => String = $sess.cmd90$$$Lambda$2796/1082786554@2a3983b9
@ func("hello", 3)
res91: String = "hellohellohello"
Above, we use the underscore _
to assign repeat _
to a value func
, which
is then a function object we can call. This can happen automatically, without the
_
, based on the “expected type” of the place the method is being used. For
example, if we expect func
to be a (String, Int) => String
, we can assign
repeat
to it without the _
:
@ val func: (String, Int) => String = repeat
func: (String, Int) => String = $sess.cmd92$$$Lambda$2803/669946146@46226d53
@ func("hello", 3)
res92: String = "hellohellohello"
Or by stubbing out the arguments with _
individually:
@ val func = repeat(_, _)
func: (String, Int) => String = $sess.cmd98$$$Lambda$2832/1025548997@358b1f86
This works, but has a bunch of annoying limitations. Firstly, even though you
can fully convert the method repeat
into a (String, Int) => String
value
using _
, you cannot partially convert it:
@ val func = repeat("hello", _)
cmd4.sc:1: missing parameter type for expanded function
((x$1: <error>) => repeat("hello", x$1))
val func = repeat("hello", _)
^
Compilation Failed
Unless you know the the “expected type” of func
, in which case you can
partially convert it:
@ val func: Int => String = repeat("hello", _)
func: Int => String = $sess.cmd93$$$Lambda$2808/1138545802@2c229ed2
Or you provide the type to the partially-applied-function-argument _
manually:
@ repeat("hello", _: Int)
res4: Int => String = $sess.cmd4$$$Lambda$1988/1407003104@5eadc347
This is a bit strange to me. If I can easily convert the entire repeat
method
into a function without specifying any types, why can I not convert it into a
function if I already know one of the arguments? After all, I have provided
strictly more information in the repeat("hello", _)
case than I have in the
repeat(_, _)
case, and yet somehow type inference got worse!
Furthermore, there’s a more fundamental issue: if I know that repeat
is a
method that takes two arguments, why can’t I just do this?
@ val func = repeat
cmd99.sc:1: missing argument list for method repeat in object cmd88
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writing `repeat _` or `repeat(_,_)` instead of `repeat`.
val func = repeat
^
Compilation Failed
After all, since the compiler already knows that repeat
is a method, and that
it doesn’t have it’s arguments provided, why not convert it for me? Why force me
to go through the _
or (_, _)
dance, or why ask me to provide an expected type
for func
if it already knows the type of repeat
?
In other languages with first-class functions, like Python, this works fine:
>>> def repeat(s, i):
... return s * i
...
>>> func = repeat
>>> func("hello", 3)
'hellohellohello'
The lack of automatic eta-expansion results in people writing weird code to work around it, such as this example from ScalaMock:
"drawLine" should "interact with Turtle" in {
// Create mock Turtle object
val mockedTurtle = mock[Turtle]
// Set expectations
(mockedTurtle.setPosition _).expects(10.0, 10.0)
(mockedTurtle.forward _).expects(5.0)
(mockedTurtle.getPosition _).expects().returning(15.0, 10.0)
// Exercise System Under Test
drawLine(mockedTurtle, (10.0, 10.0), (15.0, 10.0))
}
Here, the weird (foo _)
dance is something that they have to do purely
because of this restriction in eta-expansion.
While I’m sure there are good implementation-reasons why this doesn’t work, I don’t see any reason this shouldn’t work from a language-semantics point of view. From a user’s point of view, methods and functions are just “things you call”, and Scala is generally successful and not asking you to think about the distinction between them.
However, in cases like this, I think there isn’t a good reason the compiler
shouldn’t try a bit harder to figure out what I want before giving up and
asking me to pepper _
s or expected types all over the place. The compiler
already has all the information it needs - after all, it works if you put
an _
after the method - and it just needs to use that information when
the _
isn’t present.
Issue Analytics
- State:
- Created 6 years ago
- Reactions:20
- Comments:20 (17 by maintainers)
Top GitHub Comments
After discussing this with @adriaanm, we are leaning towards the following compromise proposal for handling references to unapplied methods
m
:If
m
has one or more parameters, we always eta expandif
m
is nullary (i.e. has type()R
):() => T
, we eta expand.m
is defined by Java, or overrides a Java defined method, we insert()
.Unapplied nullary methods are only converted to functions when a function type is expected. You need to either apply the method to
()
, or convert it to a function with() => m()
.The syntax
m _
is deprecated.Martin and I have both been working on an implementation for this 😃