Skip to content

Add escape hatch Selectable.WithoutPreciseParameterTypes #12268

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ class Definitions {
@tu lazy val LanguageDeprecatedModule: Symbol = requiredModule("scala.language.deprecated")
@tu lazy val NonLocalReturnControlClass: ClassSymbol = requiredClass("scala.runtime.NonLocalReturnControl")
@tu lazy val SelectableClass: ClassSymbol = requiredClass("scala.Selectable")
@tu lazy val WithoutPreciseParameterTypesClass: Symbol = requiredClass("scala.Selectable.WithoutPreciseParameterTypes")

@tu lazy val ReflectPackageClass: Symbol = requiredPackage("scala.reflect.package").moduleClass
@tu lazy val ClassTagClass: ClassSymbol = requiredClass("scala.reflect.ClassTag")
Expand Down
8 changes: 6 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1778,10 +1778,14 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
// then the symbol referred to in the subtype must have a signature that coincides
// in its parameters with the refinement's signature. The reason for the check
// is that if the refinement does not refer to a member symbol, we will have to
// resort to reflection to invoke the member. And reflection needs to know exact
// erased parameter types. See neg/i12211.scala.
// resort to reflection to invoke the member. And Java reflection needs to know exact
// erased parameter types. See neg/i12211.scala. Other reflection algorithms could
// conceivably dispatch without knowning precise parameter signatures. One can signal
// this by inheriting from the `scala.reflect.SignatureCanBeImprecise` marker trait,
// in which case the signature test is elided.
def sigsOK(symInfo: Type, info2: Type) =
tp2.underlyingClassRef(refinementOK = true).member(name).exists
|| tp2.derivesFrom(defn.WithoutPreciseParameterTypesClass)
|| symInfo.isInstanceOf[MethodType]
&& symInfo.signature.consistentParams(info2.signature)

Expand Down
29 changes: 25 additions & 4 deletions docs/docs/reference/changed-features/structural-types-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,19 @@ conversion that can turn `v` into a `Selectable`, and the selection methods coul
val b: { def put(x: String): Unit } = a // error
b.put("abc") // looks for a method with a `String` parameter
```
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.

The usual reflection dispatch algorithms need to know exact erased parameter types. For instance, if the example above would typecheck, the call
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 some (as yet unknown) form of 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.

Most reflection dispatch algorithms need to know exact erased parameter types. For instance, if the example above would typecheck, the call
`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`.

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:
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:

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

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:

```scala
trait MultiMethodSelectable extends Selectable.WithoutPreciseParameterTypes:
// Assume this version of `applyDynamic` can be implemented without knowing
// precise parameter types `paramTypes`:
def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ???

class Sink[A] extends MultiMethodSelectable:
def put(x: A): Unit = {}

val a = new Sink[String]
val b: MultiMethodSelectable { def put(x: String): Unit } = a // OK
```
## Differences with Scala 2 Structural Types

- Scala 2 supports structural types by means of Java reflection. Unlike
Expand Down
18 changes: 18 additions & 0 deletions library/src/scala/Selectable.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package scala

import scala.annotation.experimental

/** A marker trait for objects that support structural selection via
* `selectDynamic` and `applyDynamic`
*
Expand Down Expand Up @@ -34,3 +36,19 @@ object Selectable:
implicit def reflectiveSelectableFromLangReflectiveCalls(x: Any)(
using scala.languageFeature.reflectiveCalls): scala.reflect.Selectable =
scala.reflect.Selectable.reflectiveSelectable(x)

/** A marker trait for subclasses of `Selectable` indicating
* that precise parameter types are not needed for method dispatch. That is,
* a class inheriting from this trait and implementing
*
* def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*)
*
* should dispatch to a method with the given `name` without having to rely
* on the precise `paramTypes`. Subtypes of `WithoutPreciseParameterTypes`
* can have more relaxed subtyping rules for refinements. They do not need
* the additional restriction that the signatures of the refinement and
* the definition that implements the refinment must match.
*/
@experimental
trait WithoutPreciseParameterTypes extends Selectable
end Selectable
1 change: 1 addition & 0 deletions library/src/scala/reflect/Selectable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ object Selectable:

@inline // important for Scala.js
private final class DefaultSelectable(override protected val selectedValue: Any) extends Selectable
end Selectable
1 change: 1 addition & 0 deletions project/MiMaFilters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ object MiMaFilters {
exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeMember"),
exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#SymbolMethods.typeMembers"),
exclude[ReversedMissingMethodProblem]("scala.quoted.Quotes#reflectModule#TermParamClauseMethods.isErased"),
exclude[MissingClassProblem]("scala.Selectable$WithoutPreciseParameterTypes")
)
}
11 changes: 11 additions & 0 deletions tests/pos/i12211.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ class BB[T]

def test3: (a: AA) => (b: BB[a.type]) => BB[?] =
(a: AA) => (b: BB[a.type]) => b

@annotation.experimental // TODO: Remove once WithoutPreciseParameterTypes is no longer experimental
trait RelaxedSelectable extends Selectable.WithoutPreciseParameterTypes:
def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ???
@annotation.experimental // TODO: Remove once WithoutPreciseParameterTypes is no longer experimental
class Sink[A] extends RelaxedSelectable {
def put(x: A): Unit = {}
}
val a = new Sink[String]
val b: RelaxedSelectable { def put(x: String): Unit } = a
val _ = b.put("")