question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Scala Wart: Weak eta-expansion

See original GitHub issue

Opening 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:closed
  • Created 6 years ago
  • Reactions:20
  • Comments:20 (17 by maintainers)

github_iconTop GitHub Comments

12reactions
oderskycommented, Jun 5, 2017

After discussing this with @adriaanm, we are leaning towards the following compromise proposal for handling references to unapplied methods m:

  1. If m has one or more parameters, we always eta expand

  2. if m is nullary (i.e. has type ()R):

    1. If the expected type is of the form () => T, we eta expand.
    2. If m is defined by Java, or overrides a Java defined method, we insert ().
    3. Otherwise we issue an error of the form:

    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().

  3. The syntax m _ is deprecated.

6reactions
adriaanmcommented, Jun 5, 2017

repeat(“hello”, _)

Martin and I have both been working on an implementation for this 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

Warts of the Scala Programming Language
Warts. Weak eta-expansion; Letting callers of zero-parameter methods decide how many parens to use; Needing curlies/case for destructuring ...
Read more >
Warts of the Scala Programming Language - Reddit
r/scala - Warts of the Scala Programming Language ... Weak eta-expansion ... The automatic eta-expansion of foo when the expected type happens to...
Read more >
Eta Expansion | Scala 3 — Book
Eta Expansion is the Scala technology that lets you use methods just like functions; The technology has been improved in Scala 3 to...
Read more >
Scala 3 Dotty - OVO Tech Blog
Introduction to scala 3 and dotty syntax and features. ... more about this wart in Lihaoyi's excellent blog Scala Warts - Weak Eta...
Read more >
Overview - Scala 3
DelayedInit, existential types, weak conformance. ... Eta expansion is now performed universally also in the absence of an expected type.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found