Skip to content

Commit 533533f

Browse files
oderskysmarter
authored andcommitted
Add escape hatch Selectable.WithoutPreciseParameterTypes
1 parent 0d1d47a commit 533533f

File tree

7 files changed

+61
-6
lines changed

7 files changed

+61
-6
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

+1
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,7 @@ class Definitions {
762762
@tu lazy val LanguageDeprecatedModule: Symbol = requiredModule("scala.language.deprecated")
763763
@tu lazy val NonLocalReturnControlClass: ClassSymbol = requiredClass("scala.runtime.NonLocalReturnControl")
764764
@tu lazy val SelectableClass: ClassSymbol = requiredClass("scala.Selectable")
765+
@tu lazy val WithoutPreciseParameterTypesClass: Symbol = requiredClass("scala.Selectable.WithoutPreciseParameterTypes")
765766

766767
@tu lazy val ReflectPackageClass: Symbol = requiredPackage("scala.reflect.package").moduleClass
767768
@tu lazy val ClassTagClass: ClassSymbol = requiredClass("scala.reflect.ClassTag")

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

+6-2
Original file line numberDiff line numberDiff line change
@@ -1778,10 +1778,14 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
17781778
// then the symbol referred to in the subtype must have a signature that coincides
17791779
// in its parameters with the refinement's signature. The reason for the check
17801780
// is that if the refinement does not refer to a member symbol, we will have to
1781-
// resort to reflection to invoke the member. And reflection needs to know exact
1782-
// erased parameter types. See neg/i12211.scala.
1781+
// resort to reflection to invoke the member. And Java reflection needs to know exact
1782+
// erased parameter types. See neg/i12211.scala. Other reflection algorithms could
1783+
// conceivably dispatch without knowning precise parameter signatures. One can signal
1784+
// this by inheriting from the `scala.reflect.SignatureCanBeImprecise` marker trait,
1785+
// in which case the signature test is elided.
17831786
def sigsOK(symInfo: Type, info2: Type) =
17841787
tp2.underlyingClassRef(refinementOK = true).member(name).exists
1788+
|| tp2.derivesFrom(defn.WithoutPreciseParameterTypesClass)
17851789
|| symInfo.isInstanceOf[MethodType]
17861790
&& symInfo.signature.consistentParams(info2.signature)
17871791

docs/docs/reference/changed-features/structural-types-spec.md

+25-4
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,19 @@ conversion that can turn `v` into a `Selectable`, and the selection methods coul
100100
val b: { def put(x: String): Unit } = a // error
101101
b.put("abc") // looks for a method with a `String` parameter
102102
```
103-
The second to last line is not well-typed, since the erasure of the parameter type of `put` in class `Sink` is `Object`, but the erasure of `put`'s parameter in the type of `b` is `String`. This additional condition is necessary, since we will have to resort to reflection to call a structural member like `put` in the type of `b` above. The condition ensures that the statically known parameter types of the refinement correspond up to erasure to the parameter types of the selected call target at runtime.
104-
105-
The usual reflection dispatch algorithms need to know exact erased parameter types. For instance, if the example above would typecheck, the call
103+
The second to last line is not well-typed,
104+
since the erasure of the parameter type of `put` in class `Sink` is `Object`,
105+
but the erasure of `put`'s parameter in the type of `b` is `String`.
106+
This additional condition is necessary, since we will have to resort
107+
to some (as yet unknown) form of reflection to call a structural member
108+
like `put` in the type of `b` above. The condition ensures that the statically
109+
known parameter types of the refinement correspond up to erasure to the
110+
parameter types of the selected call target at runtime.
111+
112+
Most reflection dispatch algorithms need to know exact erased parameter types. For instance, if the example above would typecheck, the call
106113
`b.put("abc")` on the last line would look for a method `put` in the runtime type of `b` that takes a `String` parameter. But the `put` method is the one from class `Sink`, which takes an `Object` parameter. Hence the call would fail at runtime with a `NoSuchMethodException`.
107114

108-
One might hope for a "more intelligent" reflexive dispatch algorithm that does not require exact parameter type matching. Unfortunately, this can always run into ambiguities. For instance, continuing the example above, we might introduce a new subclass `Sink1` of `Sink` and change the definition of `a` as follows:
115+
One might hope for a "more intelligent" reflexive dispatch algorithm that does not require exact parameter type matching. Unfortunately, this can always run into ambiguities, as long as overloading is a possibility. For instance, continuing the example above, we might introduce a new subclass `Sink1` of `Sink` and change the definition of `a` as follows:
109116

110117
```scala
111118
class Sink1[A] extends Sink[A] { def put(x: "123") = ??? }
@@ -116,6 +123,20 @@ conversion that can turn `v` into a `Selectable`, and the selection methods coul
116123
types `Object` and `String`, respectively. Yet dynamic dispatch still needs to go
117124
to the first `put` method, even though the second looks like a better match.
118125

126+
For the cases where we can in fact implement reflection without knowing precise parameter types (for instance if static overloading is replaced by dynamically dispatched multi-methods), there is an escape hatch. For types that extend `scala.Selectable.WithoutPreciseParameterTypes` the signature check is omitted. Example:
127+
128+
```scala
129+
trait MultiMethodSelectable extends Selectable.WithoutPreciseParameterTypes:
130+
// Assume this version of `applyDynamic` can be implemented without knowing
131+
// precise parameter types `paramTypes`:
132+
def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ???
133+
134+
class Sink[A] extends MultiMethodSelectable:
135+
def put(x: A): Unit = {}
136+
137+
val a = new Sink[String]
138+
val b: MultiMethodSelectable { def put(x: String): Unit } = a // OK
139+
```
119140
## Differences with Scala 2 Structural Types
120141

121142
- Scala 2 supports structural types by means of Java reflection. Unlike

library/src/scala/Selectable.scala

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package scala
22

3+
import scala.annotation.experimental
4+
35
/** A marker trait for objects that support structural selection via
46
* `selectDynamic` and `applyDynamic`
57
*
@@ -34,3 +36,19 @@ object Selectable:
3436
implicit def reflectiveSelectableFromLangReflectiveCalls(x: Any)(
3537
using scala.languageFeature.reflectiveCalls): scala.reflect.Selectable =
3638
scala.reflect.Selectable.reflectiveSelectable(x)
39+
40+
/** A marker trait for subclasses of `Selectable` indicating
41+
* that precise parameter types are not needed for method dispatch. That is,
42+
* a class inheriting from this trait and implementing
43+
*
44+
* def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*)
45+
*
46+
* should dispatch to a method with the given `name` without having to rely
47+
* on the precise `paramTypes`. Subtypes of `SignatureCanBeImprecise`
48+
* can have more relaxed subtyping rules for refinements. They do not need
49+
* the additional restriction that the signatures of the refinement and
50+
* the definition that implements the refinment must match.
51+
*/
52+
@experimental
53+
trait WithoutPreciseParameterTypes extends Selectable
54+
end Selectable

library/src/scala/reflect/Selectable.scala

+1
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ object Selectable:
4949

5050
@inline // important for Scala.js
5151
private final class DefaultSelectable(override protected val selectedValue: Any) extends Selectable
52+
end Selectable

project/MiMaFilters.scala

+1
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ object MiMaFilters {
2727
exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeMember"),
2828
exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeMembers"),
2929
exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TermParamClauseMethods.isErased"),
30+
exclude[MissingClassProblem]("scala.Selectable$WithoutPreciseParameterTypes")
3031
)
3132
}

tests/pos/i12211.scala

+9
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,12 @@ class BB[T]
1919

2020
def test3: (a: AA) => (b: BB[a.type]) => BB[?] =
2121
(a: AA) => (b: BB[a.type]) => b
22+
23+
trait RelaxedSelectable extends Selectable.WithoutPreciseParameterTypes:
24+
def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ???
25+
class Sink[A] extends RelaxedSelectable {
26+
def put(x: A): Unit = {}
27+
}
28+
val a = new Sink[String]
29+
val b: RelaxedSelectable { def put(x: String): Unit } = a
30+
val _ = b.put("")

0 commit comments

Comments
 (0)