From 649a6ac7e0a50bf2292547fe5cce6150a91ca1a0 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 30 Mar 2021 11:12:22 +0200 Subject: [PATCH 1/2] Treat functional interfaces specially in overloading resolution Overloading resolution had two stages to determine when an alternative is applicable" 1. Match with subtyping only 2. Match with implicit conversions and SAM conversions If some alternatives are eligible under (1), only those alternatives are considered. If no alternatives are eligible under (1), alternatives are searched using (2). This behavior is different from Scala 2, which seems to use SAM conversions more aggressively. I am not sure what Scala 2 does. One obvious change would be to allow SAM conversions under (1). But I am reluctant to do that, since a SAM conversion can easily be introduced by accident. For instance, we might have two overloaded variants like this: ```scala def foo(x: C) = ... def foo(x: A => B) = ... foo((x: A) => x.toB) ``` Now, if `C` happens to be a SAM type that has an abstract method from A to B, this would be ambiguous. Generally, it feels like a SAM conversion could too easily be considered by accident here. On the other hand, if `C` was marked with `@FunctionalInterface` it would explicitly expect function arguments, so then we should treat it as morally equivalent to a function type in Scala. This is what this commit does. It refines the priority for searching applicable methods as follows: 1. Match with subtyping and with SAM conversions to functional interfaces 2. Match with implicit conversions and SAM conversions to other types --- .../dotty/tools/dotc/typer/Applications.scala | 17 +++++++++-------- tests/run/i11938.scala | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 tests/run/i11938.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 48a5738b4074..7961f74035e8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -635,15 +635,16 @@ trait Applications extends Compatibility { // matches expected type false case argtpe => - def SAMargOK = formal match { - case SAMType(sam) => argtpe <:< sam.toFunctionType(isJava = formal.classSymbol.is(JavaDefined)) - case _ => false - } + def SAMargOK(onlyFunctionalInterface: Boolean) = + (!onlyFunctionalInterface || formal.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot)) + && formal.match + case SAMType(sam) => argtpe <:< sam.toFunctionType(isJava = formal.classSymbol.is(JavaDefined)) + case _ => false if argMatch == ArgMatch.SubType then - argtpe relaxed_<:< formal.widenExpr + (argtpe relaxed_<:< formal.widenExpr) || SAMargOK(onlyFunctionalInterface = true) else isCompatible(argtpe, formal) - || ctx.mode.is(Mode.ImplicitsEnabled) && SAMargOK + || ctx.mode.is(Mode.ImplicitsEnabled) && SAMargOK(onlyFunctionalInterface = false) || argMatch == ArgMatch.CompatibleCAP && { val argtpe1 = argtpe.widen @@ -1877,12 +1878,12 @@ trait Applications extends Compatibility { record("resolveOverloaded.FunProto", alts.length) val alts1 = narrowBySize(alts) - //report.log(i"narrowed by size: ${alts1.map(_.symbol.showDcl)}%, %") + overload.println(i"narrowed by size: ${alts1.map(_.symbol.showDcl)}%, %") if isDetermined(alts1) then alts1 else record("resolveOverloaded.narrowedBySize", alts1.length) val alts2 = narrowByShapes(alts1) - //report.log(i"narrowed by shape: ${alts2.map(_.symbol.showDcl)}%, %") + overload.println(i"narrowed by shape: ${alts2.map(_.symbol.showDcl)}%, %") if isDetermined(alts2) then alts2 else record("resolveOverloaded.narrowedByShape", alts2.length) diff --git a/tests/run/i11938.scala b/tests/run/i11938.scala new file mode 100644 index 000000000000..d68d633932d9 --- /dev/null +++ b/tests/run/i11938.scala @@ -0,0 +1,14 @@ +import java.util.function.Function + +class Future[T](val initial: T) { + def map[V](v: V): Unit = println(v) + //def map(v: String): Unit = println(v) + def map[U](fn: Function[T, U]): Unit = println(fn(initial)) +} + +object Test { + val f = new Future(42) + val fn = (i: Int) => i.toString + def main(args: Array[String]): Unit = + f.map((i: Int) => i.toString) +} \ No newline at end of file From 7508d41c7caf150314c6418b600c4db1b665945f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 3 Apr 2021 11:57:42 +0200 Subject: [PATCH 2/2] More tests --- tests/neg/i11899.scala | 10 ++++++++++ tests/run/i11938.scala | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/neg/i11899.scala diff --git a/tests/neg/i11899.scala b/tests/neg/i11899.scala new file mode 100644 index 000000000000..3ae8fc496bfa --- /dev/null +++ b/tests/neg/i11899.scala @@ -0,0 +1,10 @@ +import java.util.function.Function + +class Future[T](val initial: T) { + def map[V](v: V): Unit = println(v) + def map[U](fn: Function[T, U]): Unit = println(fn(initial)) +} + +val f = new Future(42) +val fn = (i: Int) => i.toString +def test = f.map(fn) // error diff --git a/tests/run/i11938.scala b/tests/run/i11938.scala index d68d633932d9..1f8acd7fc2fc 100644 --- a/tests/run/i11938.scala +++ b/tests/run/i11938.scala @@ -8,7 +8,8 @@ class Future[T](val initial: T) { object Test { val f = new Future(42) - val fn = (i: Int) => i.toString + def fn(i: Int) = i.toString def main(args: Array[String]): Unit = f.map((i: Int) => i.toString) + f.map(fn) } \ No newline at end of file