diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 4f27c7883685..f6ea39b41590 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -343,17 +343,20 @@ object TypeOps: ).suchThat(decl.matches(_)) val inheritedInfo = inherited.info val isPolyFunctionApply = decl.name == nme.apply && (parent <:< defn.PolyFunctionType) - if isPolyFunctionApply - || inheritedInfo.exists - && !decl.isClass - && decl.info.widenExpr <:< inheritedInfo.widenExpr - && !(inheritedInfo.widenExpr <:< decl.info.widenExpr) - then - val r = RefinedType(parent, decl.name, decl.info) - typr.println(i"add ref $parent $decl --> " + r) - r - else - parent + val needsRefinement = + isPolyFunctionApply + || !decl.isClass + && { + if inheritedInfo.exists then + decl.info.widenExpr <:< inheritedInfo.widenExpr + && !(inheritedInfo.widenExpr <:< decl.info.widenExpr) + else + parent.derivesFrom(defn.SelectableClass) + } + if needsRefinement then + RefinedType(parent, decl.name, decl.info) + .reporting(i"add ref $parent $decl --> " + result, typr) + else parent } def close(tp: Type) = RecType.closeOver { rt => diff --git a/docs/docs/reference/changed-features/structural-types.md b/docs/docs/reference/changed-features/structural-types.md index 9276775b7021..31221c5d63d8 100644 --- a/docs/docs/reference/changed-features/structural-types.md +++ b/docs/docs/reference/changed-features/structural-types.md @@ -123,6 +123,43 @@ New instances of `Selectable` can be defined to support means of access other than Java reflection, which would enable usages such as the database access example given at the beginning of this document. +## Local Selectable Instances + +Local and anonymous classes that extend `Selectable` get more refined types +than other classes. Here is an example: +```scala +class Vehicle extends reflect.Selectable { + val wheels: Int +} +val i3 = new Vehicle { // i3: Vehicle { val range: Int } + val wheels = 4 + val range = 240 +} +i3.range +``` +The type of `i3` in this example is `Vehicle { val range: Int }`. Hence, +`i3.range` is well-formed. Since the base class `Vehicle` does not define a `range` field or method, we need structural dispatch to access the `range` field of the anonymous class that initializes `id3`. Structural dispatch +is implemented by the base trait `reflect.Selectable` of `Vehicle`, which +defines the necessary `selectDynamic` member. + +`Vehicle` could also extend some other subclass of `scala.Selectable` that implements `selectDynamic` and `applyDynamic` differently. But if it does not extend a `Selectable` at all, the code would no longer typecheck: +```scala +class Vehicle { + val wheels: Int +} +val i3 = new Vehicle { // i3: Vehicle + val wheels = 4 + val range = 240 +} +i3.range: // error: range is not a member of `Vehicle` +``` +The difference is that the type of an anonymous class that does not extend `Selectable` is just formed from the parent type(s) of the class, without +adding any refinements. Hence, `i3` now has just type `Vehicle` and the selection `i3.range` gives a "member not found" error. + +Note that in Scala 2 all local and anonymous classes could produce values with refined types. But +members defined by such refinements could be selected only with the language import +`reflectiveCalls`. + ## Relation with `scala.Dynamic` There are clearly some connections with `scala.Dynamic` here, since diff --git a/library/src/scala/reflect/Selectable.scala b/library/src/scala/reflect/Selectable.scala index d9210d60d1a4..c4069c6abcb0 100644 --- a/library/src/scala/reflect/Selectable.scala +++ b/library/src/scala/reflect/Selectable.scala @@ -1,29 +1,43 @@ package scala.reflect -class Selectable(val receiver: Any) extends AnyVal with scala.Selectable { - def selectDynamic(name: String): Any = { - val rcls = receiver.getClass - try { +/** A class that implements structural selections using Java reflection. + * It can be used as a supertrait of a class or be made available + * as an implicit conversion via `reflectiveSelectable`. + */ +trait Selectable extends scala.Selectable: + + /** The value from which structural members are selected. + * By default this is the Selectable instance itself, but it can + * be overridden. + */ + protected def selectedValue: Any = this + + /** Select member with given name */ + def selectDynamic(name: String): Any = + val rcls = selectedValue.getClass + try val fld = rcls.getField(name) ensureAccessible(fld) - fld.get(receiver) - } - catch { - case ex: NoSuchFieldException => - applyDynamic(name)() - } - } + fld.get(selectedValue) + catch case ex: NoSuchFieldException => + applyDynamic(name)() - def applyDynamic(name: String, paramTypes: ClassTag[_]*)(args: Any*): Any = { - val rcls = receiver.getClass + /** Select method and apply to arguments. + * @param name The name of the selected method + * @param paramTypes The class tags of the selected method's formal parameter types + * @param args The arguments to pass to the selected method + */ + def applyDynamic(name: String, paramTypes: ClassTag[_]*)(args: Any*): Any = + val rcls = selectedValue.getClass val paramClasses = paramTypes.map(_.runtimeClass) val mth = rcls.getMethod(name, paramClasses: _*) ensureAccessible(mth) - mth.invoke(receiver, args.asInstanceOf[Seq[AnyRef]]: _*) - } -} + mth.invoke(selectedValue, args.asInstanceOf[Seq[AnyRef]]: _*) + +object Selectable: -object Selectable { - implicit def reflectiveSelectable(receiver: Any): Selectable = - new Selectable(receiver) -} + /** An implicit conversion that turns a value into a Selectable + * such that structural selections are performed on that value. + */ + implicit def reflectiveSelectable(x: Any): Selectable = + new Selectable { override val selectedValue = x } diff --git a/tests/run/selectable-new.scala b/tests/run/selectable-new.scala new file mode 100644 index 000000000000..e460ca631b56 --- /dev/null +++ b/tests/run/selectable-new.scala @@ -0,0 +1,11 @@ +@main def Test = + val x = + class C extends reflect.Selectable: + def name: String = "hello" + new C + + val y = new reflect.Selectable: + def name: String = "hello" + + assert(x.name == "hello") + assert(y.name == "hello")