-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Do not eta-expand 0-arg methods. Improve eta-expansion for method values. #6475
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
Conversation
d34eff5
to
0fe3b36
Compare
()
before eta-expansion, clean up
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
These calls now compile with 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 |
The dependence on syntactic function literals goes away in my
overload_proto branch, btw. Let’s discuss in a chat next week.
…On Fri, Apr 6, 2018 at 14:54 Stefan Zeiger ***@***.***> wrote:
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.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#6475 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAFjy4_dXOvy_h35zWi2RnjFRykUyztJks5tl2VqgaJpZM4S9q6f>
.
|
typed(tree0, mode, pt) | ||
} | ||
// (4.2) apply to empty argument list | ||
else if (mt.params.isEmpty && (settings.isScala213 || !isFunctionType(pt))) emptyApplication |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
3e53360
to
0ebdc2d
Compare
9a4bd27
to
4b0ef47
Compare
()
before eta-expansion, clean up
@adriaanm Should we merge this for M4 or hold off until you're done with the additional changes in overload_proto? |
I would like to merge it. Lukas hasn't officially signed off on it yet, though. Maybe @retronym can take a look? |
ping @lrytz |
fixes scala/scala-dev#391 |
@adriaanm do you want a community build run on this...? |
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.
There was a problem hiding this 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)
Thanks! |
(I'll keep an eye out for related breakage in future community build runs.) |
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 Of course, it's not the only possible replacement. |
Our assumption is that relying on automatic insertion of |
oh, wait a minute, looks like I forgot about
(from scala/scala3#2570 (comment)) So, your example should actually work. |
Hmm, isn't it the same as https://github.com/scala/scala/pull/6475/files#diff-97f418d5b560997cbbea454143f963cfR9 ? Or should that only happen under 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 |
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). |
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.
|
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... |
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 |
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> |
I think you could just as well argue that it doesn't make sense to define Since you did define mkStub to take an argument list, you should also supply it on (intended) application. |
@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())
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 🔥 |
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 I think we should deprecate along these lines in 2.14, and enforce in 3.0. |
👍 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. |
See #7660 |
I'm not sure if I grasp the whole picture here, but this part for sure:
is 💯, the way it is now has been bugging me for a decade |
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.