-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix #4364: Try SAM type when no candidates found #4821
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
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.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Have an awesome day! ☀️
case SAMType(mtp) => | ||
val sam = narrowByTypes(alts, mtp.paramInfos, mtp.resultType) | ||
if (sam.nonEmpty && !pt.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot)) | ||
ctx.warning(ex"$pt does not have the @FunctionalInterface annotation.", ctx.tree.pos) |
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.
I still don't think this is worth adding a warning for. Even if it is, there should be a simple way to turn off this warning.
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 simple way is to do explicit eta-expension, no?
new ObjectOutputStream(println) // warning: are you sure this is what you want to do?
new ObjectOutputStream(() => println()) // Ok
new ObjectOutputStream(println _) // Ok
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.
Right, but if I remember correctly, we plan to deprecate "f _" since just using "f" should work in all situations
val noSam = alts filter (normalizedCompatible(_, pt)) | ||
if (noSam.isEmpty) { | ||
pt match { | ||
case SAMType(mtp) => |
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.
I would move this case to the enclosing match
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.
val candidates = pt match {
...
case SAMType(mtp) =>
val sam = narrowByTypes(alts, mtp.paramInfos, mtp.resultType)
if (sam.nonEmpty && !pt.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot))
ctx.warning(...)
sam
case pt =>
alts filter (normalizedCompatible(_, pt))
}
Do you mean fixing the code like the above one?
I think this code would choose the second f
instead of the third f
for the below code example.
def f(x: Int): Unit = assert(false)
def f(x: String): Unit = assert(false)
def f: java.io.OutputStream = new java.io.OutputStream { def write(x: Int) = () }
val oosF = new java.io.ObjectOutputStream(f)
oosF.write(0)
Do I understand your comment correctly?
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.
I see. This deserves a comment with an example in the code
case SAMType(mtp) => | ||
val sam = narrowByTypes(alts, mtp.paramInfos, mtp.resultType) | ||
if (sam.nonEmpty && !pt.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot)) | ||
ctx.warning(ex"$pt does not have the @FunctionalInterface annotation.", ctx.tree.pos) |
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.
ctx.tree.pos
here will not be very precise. Ideally it should be the position of the tree being expended:
-- Warning: tests/allan/Test.scala:5:6 -----------------------------------------
5 | val out2 = new ObjectOutputStream(println)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Should be:
-- Warning: tests/allan/Test.scala:5:6 -----------------------------------------
5 | val out2 = new ObjectOutputStream(println)
| ^^^^^^^
tests/run/i4364a.scala
Outdated
@@ -0,0 +1,13 @@ | |||
object Test { | |||
var flag = false |
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.
I suppose you can remove this
tests/run/i4364a.scala
Outdated
|
||
def f(): Unit = assert(false) | ||
def f(x: Int): Unit = assert(false) | ||
def f(x: String): Unit = flag = true |
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.
def f(x: String): Unit = ()
@@ -0,0 +1,23 @@ | |||
object Test { |
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.
It is not clear what you are testing here that is not tested in i4364a.scala
. I suggest you add a test to tests/neg-custom-args/fatal-warnings
to test the warnings:
class Test {
def foo(c: java.util.function.Consumer[String]) = ()
def bar(out: java.io.OutputStream) = ()
bar(x => println(x))
bar(println(_))
bar(println) // error: OutputStream is not @FunctionalInterface
def f(x: String) = ()
foo(f) // Ok: Consumer is @FunctionalInterface
}
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.
def f(x: Int): Unit = assert(false)
def f(x: String): Unit = assert(false)
def f: java.io.OutputStream = new java.io.OutputStream { def write(x: Int) = () }
val oosF = new java.io.ObjectOutputStream(f)
oosF.write(0)
Through the test, I tried to check that whether method overloading is correctly resolved when candidates exist without considering SAM type. In this case, the second f
is valid choice when it is eta-expanded and OutputStream
is considered as SAM type. However, the third f
should be choosed. Therefore, I think this test needs to remain.
As you suggested, it would be better to check a warning in a seperate test. I'll remove
val oosG = new java.io.ObjectOutputStream(g)
oosG.write(0)
oosG.close()
from test b and add a test about a warning in tests/neg-custom-args/fatal-warnings
.
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.
Good point. I suggest a simpler test though:
import java.util.function.Consumer
object Test {
def foo(c: Consumer[String]) = c.accept("")
def bar(x: String): Unit = ???
def bar: Consumer[String] = new Consumer { def accept(s: String) = () }
def main(args: Array[String]) = {
foo(bar)
}
}
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.
I agree that this one is simpler and clearer. I'll fix it. Thank you!
@@ -1274,7 +1274,7 @@ trait Applications extends Compatibility { self: Typer with Dynamic => | |||
* Two trials: First, without implicits or SAM conversions enabled. Then, | |||
* if the fist finds no eligible candidates, with implicits and SAM conversions enabled. | |||
*/ | |||
def resolveOverloaded(alts: List[TermRef], pt: Type)(implicit ctx: Context): List[TermRef] = track("resolveOverloaded") { | |||
def resolveOverloaded(alts: List[TermRef], pt: Type, pos: Position = NoPosition)(implicit ctx: Context): List[TermRef] = track("resolveOverloaded") { |
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.
Do you need a default value here?
|
||
case defn.FunctionOf(args, resultType, _, _) => | ||
narrowByTypes(alts, args, resultType) | ||
|
||
case pt => | ||
alts filter (normalizedCompatible(_, pt)) | ||
val noSam = alts filter (normalizedCompatible(_, pt)) |
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.
I think noSam
is not a good variable name. Maybe alts1
val noSam = alts filter (normalizedCompatible(_, pt)) | ||
if (noSam.isEmpty) { | ||
/* | ||
* the case should not be moved to the enclosing match |
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.
Maybe instead:
// We only consider SAM types when there is no alternative.
// In the example below, the second overload of `bar` should
// be preferred over the first one:
//
// def foo(c: Consumer[String]) = ...
// def bar(x: String): Unit = ...
// def bar: Consumer[String] = ...
// foo(bar)
foo(f) // Ok: Consumer is @FunctionalInterface | ||
|
||
val oos = new java.io.ObjectOutputStream(f) // error: OutputStream is not @FunctionalInterface | ||
oos.write(0) |
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.
You can remove this line and the next one
object Test { | ||
def foo(c: java.util.function.Consumer[String]) = c.accept("") | ||
|
||
def f(x: String): Unit = () |
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.
You can remove the first overload of f
case SAMType(mtp) => | ||
val sam = narrowByTypes(alts, mtp.paramInfos, mtp.resultType) | ||
if (sam.nonEmpty && !pt.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot)) | ||
ctx.warning(ex"${sam.head.designator} is eta-expanded even though $pt does not have the @FunctionalInterface annotation.", pos) |
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.
I am not 100% sure this is the right place to emit the warning. This assumes that this overload will be chosen, and its argument will be eta-expanded . Although I couldn't come up with an example where the warning is emitted but no eta-expansion happen.
I would emit the warning where the eta-expansion is performed
@@ -2321,9 +2321,11 @@ class Typer extends Namer | |||
if (arity >= 0 && | |||
!tree.symbol.isConstructor && | |||
!ctx.mode.is(Mode.Pattern) && | |||
!(isSyntheticApply(tree) && !isExpandableApply)) | |||
!(isSyntheticApply(tree) && !isExpandableApply)) { | |||
if (!pt.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot)) |
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.
You need to exclude function classes (i.e. defn.isFunctionClass
)
I found the following comment from the 1331st line of
In the current version of the public How do you think about the following fix? Adding val SAMConversionEnabled = newMode(...) to Changing public var found = resolveOverloaded(alts, pt, Nil)(ctx.retractMode(Mode.ImplicitsEnabled).retractMode(Mode.SAMConversionEnabled)
if (found.isEmpty && (ctx.mode.is(Mode.ImplicitsEnabled) || ctx.mode.is(Mode.SAMConversionEnabled)))
found = resolveOverloaded(alts, pt, Nil) Changing private val candidates = pt match {
...
case SAMType(mtp) if ctx.mode.is(Mode.SAMConversionEnabled) =>
narrowByTypes(alts, mtp.paramInfos, mtp.resultType)
case pt =>
alts filter (normalizedCompatible(_, pt))
} |
I thought again about the above fix. It will result unnecessary redundant calculations. Would it be better to fix the comment to explain the current code correctly and keep the current code? |
Your proposed fix looked reasonable to me (except that you can reuse import java.util.function.Consumer
class Test {
def foo(c: Consumer[String]) = 1
def f(x: Int): Unit = ()
def f: Int = 1
implicit def bar(x: Int): Consumer[String] = ???
foo(f) // error: none of the overloaded alternatives of method f match expected type Consumer[String]
} So I would say, we leave it as is for now. #4831 is a related issue I found while looking at it |
!(isSyntheticApply(tree) && !isExpandableApply)) | ||
!(isSyntheticApply(tree) && !isExpandableApply)) { | ||
pt match { | ||
case SAMType(_) if !defn.isFunctionType(pt) && !pt.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot) => |
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 extractor is more expensive than the other tests, I would reorder as follow:
/** Is this a SAM type without the @FunctionalInterface annotation? */
def isNonFunctionalSamType =
!pt.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot) &&
!defn.isFunctionType(pt) &&
SAMType.unapply(pt).isDefined
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 change to overloading looks like the right thing.
I'd like to discuss a bit more the warning if the expected type of an eta expansion is not a FunctionalInterface. It might end up to be the best thing we can do, but we should discuss it once more.
Looking at the documentation of @FunctionalInterface, it seems to play a role much like @tailrec in Scala: Whether an interface counts as a functional interface is decided structurally, but a @FunctionalInterface annotation makes sure that the user gets an error of a type is not structurally a functional interface.
It seems an overstretch to use the same annotation for deciding whether to warning when eta-expanding to yield instances of SAM types. On the other hand, the status quo where we do so silently can give nasty surprises. So, possible choices are:
- don't warn (the status quo)
- make it a warning, as this PR does
- make it an error
I also have a tendency to make it a warning. But would like to get feedback from others. /cc @adriaanm
if (!pt.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot) && | ||
!defn.isFunctionType(pt) && | ||
SAMType.unapply(pt).isDefined) | ||
ctx.warning(ex"${tree.symbol} is eta-expanded even though ${pt.classSymbol} does not have the @FunctionalInterface annotation.", tree.pos) |
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.
Small nitpick: I'd change the condition to
if (!defn.isFunctionType(pt))
pt match {
case SAMType(_) if !pt.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot) =>
...
case _ =>
}
I think it's a good idea to warn. Since Java users are encouraged (though not required) to express their intent with the |
Superseded by #5717. |
No description provided.