Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not eta-expand 0-arg methods. Improve eta-expansion for method values. #6475

Merged
merged 2 commits into from
May 10, 2018

Conversation

adriaanm
Copy link
Contributor

@adriaanm adriaanm commented Mar 27, 2018

Insert () before considering eta-expansion. A small step towards Scala 3, where you will have to apply () explicitly to Scala-defined methods that define an argument list.

Also clean up method value expansion: do it directly, instead of attaching a note for later and adapting again, which fixes scala/bug#8299.

@scala-jenkins scala-jenkins added this to the 2.13.0-M4 milestone Mar 27, 2018
@adriaanm adriaanm changed the title wip: cleanup eta expansion wip: cleanup eta expansion [ci: last-only] Mar 28, 2018
@adriaanm adriaanm force-pushed the eta_cleanup branch 2 times, most recently from d34eff5 to 0fe3b36 Compare April 5, 2018 21:50
@adriaanm adriaanm changed the title wip: cleanup eta expansion [ci: last-only] Insert () before eta-expansion, clean up Apr 5, 2018
@szeiger
Copy link
Contributor

szeiger commented Apr 6, 2018

I think this goes in the right direction and it helps with the overloaded methods in the new collection library that required changes in the compiler codebase. Without -Xsource:2.14:

[error] /Users/szeiger/code/scala/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala:558: missing argument list for method mapArgInfo
[error] Unapplied methods are only converted to functions when a function type is expected.
[error] You can make this conversion explicit by writing `mapArgInfo _` or `mapArgInfo(_)` instead of `mapArgInfo`.
[error]       val argInfos = originalCallsite.argInfos.flatMap(mapArgInfo)
[error]                                                        ^
[error] /Users/szeiger/code/scala/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala:577: missing argument list for method mapArgInfo
[error] Unapplied methods are only converted to functions when a function type is expected.
[error] You can make this conversion explicit by writing `mapArgInfo _` or `mapArgInfo(_)` instead of `mapArgInfo`.
[error]       val capturedArgInfos = originalClosureInit.capturedArgInfos.flatMap(mapArgInfo)
[error]                                                                           ^
[error] /Users/szeiger/code/scala/src/compiler/scala/tools/reflect/ToolBoxFactory.scala:230: missing argument list for method makeParam
[error] Unapplied methods are only converted to functions when a function type is expected.
[error] You can make this conversion explicit by writing `makeParam _` or `makeParam(_)` instead of `makeParam`.
[error]           meth setInfo MethodType(freeTerms.map(makeParam).toList, AnyTpe)
[error]                                                 ^

These calls now compile with -Xsource:2.14 because the methods are monomorphic.

There's still the problem of the dual meaning of "expected type" in overload resolution. When we know that we're typechecking a lambda we can set it to a lub of the parameter types (i.e. a glb of the function type) but for typechecking other constructs that would be wrong (you need the opposite variance). While the more aggressive expansion will attempt to typecheck a method reference as a lambda it doesn't know about the parameter types that are necessary to do that.

This limitation causes a regressions due to the new collection library in cases like this:

    def iden[T](x: T): T = x
    val v = scala.collection.immutable.BitSet(1,2,3).map(iden)
    val vt: scala.collection.immutable.BitSet = v

These are the ones where an explicit _ is not sufficient as a work-around, you have to write map(x => iden(x)) to make overload resultion supply the correct parameter types that allow typing of the polymorphic lambda.

@adriaanm
Copy link
Contributor Author

adriaanm commented Apr 6, 2018 via email

typed(tree0, mode, pt)
}
// (4.2) apply to empty argument list
else if (mt.params.isEmpty && (settings.isScala213 || !isFunctionType(pt))) emptyApplication
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isScala213 is true for 2.13+, so should it be !isScala214 here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent is to go the other way: anything < 2.13 needs to check the expected type before inserting the empty apply. From 2.13, we’ll always insert them. I’ll add a comment to clarify

@adriaanm adriaanm force-pushed the eta_cleanup branch 2 times, most recently from 3e53360 to 0ebdc2d Compare April 11, 2018 15:17
@adriaanm adriaanm force-pushed the eta_cleanup branch 2 times, most recently from 9a4bd27 to 4b0ef47 Compare April 26, 2018 13:28
@adriaanm adriaanm changed the title Insert () before eta-expansion, clean up Do not eta-expand 0-arg methods. Improve eta-expansion for method values. Apr 26, 2018
@szeiger
Copy link
Contributor

szeiger commented May 3, 2018

@adriaanm Should we merge this for M4 or hold off until you're done with the additional changes in overload_proto?

@adriaanm
Copy link
Contributor Author

adriaanm commented May 3, 2018

I would like to merge it. Lukas hasn't officially signed off on it yet, though. Maybe @retronym can take a look?

@adriaanm
Copy link
Contributor Author

adriaanm commented May 9, 2018

ping @lrytz

@SethTisue
Copy link
Member

fixes scala/scala-dev#391

@SethTisue
Copy link
Member

@adriaanm do you want a community build run on this...?

@adriaanm
Copy link
Contributor Author

adriaanm commented May 9, 2018

Hadn't thought about it, but I guess it wouldn't hurt :-)

Also eta-expand method values directly,
rather than going through an adapt,
which then needs to remember that an _
followed the method value
We'll use the expected type when we type the eta-expansion
that was requested.

We know `m` is expected to be a method, since only methods
can be followed by `_` to request eta-expansion. So,
have the expected type reflect that. Since the original
expected type won't be compatible with the method type
that FUNmode gives us (method values are not first class),
we have to defer using the given expected type until we
eta-expanded, which results in a function, which is first class.

When typing under FUNmode under pt=* we can let implicit
conversion do its thing, before we wrap this in a function.

See scala/bug#8299.
Copy link
Member

@lrytz lrytz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Push--f'd to your branch, added some tests (diff https://gist.github.com/lrytz/82e301f22610ce7d1b604a069db8b2fc)

@adriaanm
Copy link
Contributor Author

Thanks!

@adriaanm adriaanm merged commit 8951701 into scala:2.13.x May 10, 2018
@SethTisue
Copy link
Member

(I'll keep an eye out for related breakage in future community build runs.)

@jrudolph
Copy link
Contributor

Is there any more documentation about why this change was done? Is it for implementation reasons? Or is it a temporary thing needed for an incremental improvement to saner defaults in the upcoming versions?

I see that it is somehow related to 0-arg methods still being allowed to be called without parentheses (why?). Why not change that behavior and keep eta-expansion intact? It's an incompatible change anyway.

While trying to migrate code to 2.13 (in https://github.com/akka/akka-http/pull/2298/files#diff-10c929125531820c5fea3db482903b3eR41) we now have the situation where for

def f0(): Int = 23
def f1(a: Int): Int = 42
def g(f0: () => Int, f1: Int => Int): Int = ???

in 2.12 we have

g(f0, f1)

while for 2.13 a valid replacement is

g(() => f0, f1)

which seems odd. First, it's now inconsistent with the 1-arg case and also the f0 application still doesn't require parentheses.

Of course, it's not the only possible replacement. g(f0 _, f1) might still work but it seems method values are also being deprecated in the future?

@adriaanm
Copy link
Contributor Author

Our assumption is that relying on automatic insertion of () is more common than eta-expansion of a 0-arg method, and thus changing the latter affects fewer users than the former.

@adriaanm
Copy link
Contributor Author

adriaanm commented Jan 16, 2019

oh, wait a minute, looks like I forgot about

If the expected type is of the form () => T, we eta expand.

(from scala/scala3#2570 (comment))

So, your example should actually work.

@jrudolph
Copy link
Contributor

Hmm, isn't it the same as https://github.com/scala/scala/pull/6475/files#diff-97f418d5b560997cbbea454143f963cfR9 ? Or should that only happen under -Xsource:2.14?

Welcome to Scala 2.13.0-M5 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_181).
Type in expressions for evaluation. Or try :help.

scala> def f0(): Int = 42
f0: ()Int

scala> def g(f0: () => Int) = ???
g: (f0: () => Int)Nothing

scala> g(f0)
         ^
       error: type mismatch;
        found   : Int
        required: () => Int

@adriaanm
Copy link
Contributor Author

I was confused (well, we did go back and forth on this a few times), but the latest decision was to allow eta expansion if the expected type demands it. I'll create a follow up PR today

@jrudolph
Copy link
Contributor

I was confused (well, we did go back and forth on this a few times), but the latest decision was to allow eta expansion if the expected type demands it. I'll create a follow up PR today

But is it still possible given the spec update to do Empty Application before eta expansion? It seems the M5 behavior is at least following the spec (even if that's not what I would like).

@adriaanm
Copy link
Contributor Author

adriaanm commented Jan 16, 2019

Hmm. 🤔 Now that I've refreshed my recollection of the motivation (see especially scala/bug#9178), I wonder whether a type-driven exception is a good idea. For dotty alignment, we'd have to do it, but the puzzlers are puzzling.

scala/bug#7187 deprecates eta-expansion of zero-arg method values (since 2.12; was never introduced for SAM types).
The next step is to also deprecate insertion of () (except for java-defined methods), as dotty already requires explicitly writing them.
Once explicit application to () is required, we can more aggressively eta-expand method references, even if they are 0-arity

@jrudolph
Copy link
Contributor

Yep, this issue (like so many others) seems to have connections and inter-dependencies with so many other things it's quite hard to keep track of motivations and current progress...

@adriaanm
Copy link
Contributor Author

I just checked the status quo in dotty (https://github.com/dotty-staging/dotty/blob/master/compiler/src/dotty/tools/dotc/typer/Typer.scala#L2491), and it looks like 0-ary eta expansion is performed regardless of expected type (well, there's a warning when the SAM type is not annotated as FunctionalInterface). I just happened to be in the neighbourhood yesterday but missed this. /cc @odersky @smarter

@adriaanm
Copy link
Contributor Author

adriaanm commented Jan 16, 2019

Since this issue is spread out over so many tickets, here's the motivation for deprecating eta-expansion in this case:

def mkStub(): () => String = () => ""
val stub: () => Any = mkStub

println(stub()) // <function0>

@adriaanm
Copy link
Contributor Author

I think you could just as well argue that it doesn't make sense to define mkStub with an empty argument list, since it's pure in the sense that it doesn't mutate any potentially aliased objects.

Since you did define mkStub to take an argument list, you should also supply it on (intended) application.

@adriaanm
Copy link
Contributor Author

@jrudolph do you have a real world example in akka or something? In the abstract, it's hard to decide.

@dwijnand
Copy link
Member

@jrudolph
Copy link
Contributor

@jrudolph do you have a real world example in akka or something? In the abstract, it's hard to decide.

Yes, one instance is in https://github.com/akka/akka-http/pull/2298/files#diff-10c929125531820c5fea3db482903b3eR41. But I think it's hard to discuss concretely because every single case will be quite insignificant when there's a simple workaround like here. I just stumbled over it because in that example we have both Function1 and Function0 next to each other and it shows how the new behavior isn't consistent any more.

It was

def encodeChunk(bytes: ByteString): ByteString = ...
def finish(): ByteString = ...

StreamUtils.byteStringTransformer(encodeChunk, finish)

Now it has to be

StreamUtils.byteStringTransformer(encodeChunk, () => finish())

Since this issue is spread out over so many tickets, here's the motivation for deprecating eta-expansion in this case:

def mkStub(): () => String = () => ""
val stub: () => Any = mkStub

println(stub()) // <function0>

In my opinion, this example works as intended. Empty application seems much more like a convenience feature than eta-expansion (but your mileage may vary) so I'd wish for eta-expansion to take precedence. You could go further and say why would you create a 0-arg method at all in Scala if not to mean it to be side-effecting? In that case empty application seems downright dangerous 🔥

@adriaanm
Copy link
Contributor Author

I tend to agree. I think our (future) rule for auto-application should be even stricter than it is in dotty right now. You only get a free () when invoking a Java-defined method, otherwise, if the method is defined with an argument list, you must supply it. A Scala method overriding a Java method also gets an exemption: it may drop the empty argument list defined in Java (because you can't express no-argument list in Java), and then any (statically known) application of that argument-list-free method of course needn't supply the empty argument list.

I think we should deprecate along these lines in 2.14, and enforce in 3.0.

@jrudolph
Copy link
Contributor

👍 sounds like a good plan for me.

I think the conflict will then still remains (and a different solution might be chosen) for calling Java methods or creating SAMs of Java interfaces like the one shown in scala/bug#9178.

@adriaanm
Copy link
Contributor Author

See #7660

@SethTisue
Copy link
Member

SethTisue commented Jan 17, 2019

I'm not sure if I grasp the whole picture here, but this part for sure:

You only get a free () when invoking a Java-defined method

is 💯, the way it is now has been bugging me for a decade

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes worth highlighting in next release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

eta-expansion for method on target that needs implicit conversion
7 participants