Skip to content

Commit ebe223e

Browse files
authored
Merge pull request #9180 from dotty-staging/structural-new
Change structural types scheme
2 parents ddc4dc9 + b5b48cd commit ebe223e

File tree

11 files changed

+256
-85
lines changed

11 files changed

+256
-85
lines changed

compiler/src/dotty/tools/dotc/typer/Dynamic.scala

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,24 @@ trait Dynamic {
131131
* and `x.a` is of type `U`, map `x.a` to the equivalent of:
132132
*
133133
* ```scala
134-
* (x: Selectable).selectDynamic("a").asInstanceOf[U]
134+
* x1.selectDynamic("a").asInstanceOf[U]
135135
* ```
136+
* where `x1` is `x` adapted to `Selectable`.
136137
*
137138
* Given `x.a(a11, ..., a1n)...(aN1, ..., aNn)`, where `x.a` is of (widened) type
138-
* `(T11, ..., T1n)...(TN1, ..., TNn) => R`, it is desugared to:
139+
* `(T11, ..., T1n)...(TN1, ..., TNn): R`, it is desugared to:
139140
*
140141
* ```scala
141-
* (x:selectable).applyDynamic("a", CT11, ..., CT1n, ..., CTN1, ... CTNn)
142-
* (a11, ..., a1n, ..., aN1, ..., aNn)
143-
* .asInstanceOf[R]
142+
* x1.applyDynamic("a")(a11, ..., a1n, ..., aN1, ..., aNn)
143+
* .asInstanceOf[R]
144+
* ```
145+
* If this call resolves to an `applyDynamic` method that takes a `ClassTag[?]*` as second
146+
* parameter, we further rewrite this call to
147+
* scala```
148+
* x1.applyDynamic("a", CT11, ..., CT1n, ..., CTN1, ... CTNn)
149+
* (a11, ..., a1n, ..., aN1, ..., aNn)
150+
* .asInstanceOf[R]
144151
* ```
145-
*
146152
* where CT11, ..., CTNn are the class tags representing the erasure of T11, ..., TNn.
147153
*
148154
* It's an error if U is neither a value nor a method type, or a dependent method
@@ -151,20 +157,37 @@ trait Dynamic {
151157
def handleStructural(tree: Tree)(using Context): Tree = {
152158
val (fun @ Select(qual, name), targs, vargss) = decomposeCall(tree)
153159

154-
def structuralCall(selectorName: TermName, ctags: List[Tree]) = {
160+
def structuralCall(selectorName: TermName, ctags: => List[Tree]) = {
155161
val selectable = adapt(qual, defn.SelectableClass.typeRef)
156162

157-
// ($qual: Selectable).$selectorName("$name", ..$ctags)
163+
// ($qual: Selectable).$selectorName("$name")
158164
val base =
159165
untpd.Apply(
160166
untpd.TypedSplice(selectable.select(selectorName)).withSpan(fun.span),
161-
(Literal(Constant(name.toString)) :: ctags).map(untpd.TypedSplice(_)))
167+
(Literal(Constant(name.toString)) :: Nil).map(untpd.TypedSplice(_)))
162168

163169
val scall =
164170
if (vargss.isEmpty) base
165171
else untpd.Apply(base, vargss.flatten)
166172

167-
typed(scall)
173+
// If function is an `applyDynamic` that takes a ClassTag* parameter,
174+
// add `ctags`.
175+
def addClassTags(tree: Tree): Tree = tree match
176+
case Apply(fn: Apply, args) =>
177+
cpy.Apply(tree)(addClassTags(fn), args)
178+
case Apply(fn @ Select(_, nme.applyDynamic), nameArg :: _ :: Nil) =>
179+
fn.tpe.widen match
180+
case mt: MethodType => mt.paramInfos match
181+
case _ :: ctagsParam :: Nil
182+
if ctagsParam.isRepeatedParam
183+
&& ctagsParam.argInfos.head.isRef(defn.ClassTagClass) =>
184+
val ctagType = defn.ClassTagClass.typeRef.appliedTo(TypeBounds.empty)
185+
cpy.Apply(tree)(fn,
186+
nameArg :: seqToRepeated(SeqLiteral(ctags, TypeTree(ctagType))) :: Nil)
187+
case _ => tree
188+
case other => tree
189+
case _ => tree
190+
addClassTags(typed(scall))
168191
}
169192

170193
def fail(name: Name, reason: String) =
@@ -187,7 +210,7 @@ trait Dynamic {
187210
if (isDependentMethod(tpe))
188211
fail(name, i"has a method type with inter-parameter dependencies")
189212
else {
190-
val ctags = tpe.paramInfoss.flatten.map(pt =>
213+
def ctags = tpe.paramInfoss.flatten.map(pt =>
191214
implicitArgTree(defn.ClassTagClass.typeRef.appliedTo(pt.widenDealias :: Nil), fun.span.endPos))
192215
structuralCall(nme.applyDynamic, ctags).cast(tpe.finalResultType)
193216
}

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

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,61 +14,63 @@ RefineStat ::= ‘val’ VarDcl | ‘def’ DefDcl | ‘type’ {nl} TypeDcl
1414

1515
## Implementation of structural types
1616

17-
The standard library defines a trait `Selectable` in the package
18-
`scala`, defined as follows:
17+
The standard library defines a universal marker trait `Selectable` in the package `scala`:
1918

2019
```scala
21-
trait Selectable extends Any {
22-
def selectDynamic(name: String): Any
23-
def applyDynamic(name: String, paramClasses: ClassTag[_]*)(args: Any*): Any =
24-
new UnsupportedOperationException("applyDynamic")
25-
}
20+
trait Selectable extends Any
2621
```
2722

2823
An implementation of `Selectable` that relies on Java reflection is
2924
available in the standard library: `scala.reflect.Selectable`. Other
3025
implementations can be envisioned for platforms where Java reflection
3126
is not available.
3227

33-
`selectDynamic` takes a field name and returns the value associated
34-
with that name in the `Selectable`. Similarly, `applyDynamic`
35-
takes a method name, `ClassTag`s representing its parameters types and
36-
the arguments to pass to the function. It will return the result of
37-
calling this function with the given arguments.
28+
Implementations of `Selectable` have to make available one or both of
29+
the methods `selectDynamic` and `applyDynamic`. The methods could be members of the `Selectable` implementation or they could be extension methods.
30+
31+
The `selectDynamic` method takes a field name and returns the value associated with that name in the `Selectable`.
32+
It should have a signature of the form:
33+
```scala
34+
def selectDynamic(name: String): T
35+
```
36+
Often, the return type `T` is `Any`.
37+
38+
The `applyDynamic` method is used for selections that are applied to arguments. It takes a method name and possibly `ClassTag`s representing its parameters types as well as the arguments to pass to the function.
39+
Its signature should be of one of the two following forms:
40+
```scala
41+
def applyDynamic(name: String)(args: Any*): T
42+
def applyDynamic(name: String, ctags: ClassTag[?]*)(args: Any*): T
43+
```
44+
Both versions are passed the actual arguments in the `args` parameter. The second version takes in addition a vararg argument of class tags that identify the method's parameter classes. Such an argument is needed
45+
if `applyDynamic` is implemented using Java reflection, but it could be
46+
useful in other cases as well. `selectDynamic` and `applyDynamic` can also take additional context parameters in using clauses. These are resolved in the normal way at the callsite.
3847

3948
Given a value `v` of type `C { Rs }`, where `C` is a class reference
40-
and `Rs` are refinement declarations, and given `v.a` of type `U`, we
41-
consider three distinct cases:
49+
and `Rs` are structural refinement declarations, and given `v.a` of type `U`, we consider three distinct cases:
4250

43-
- If `U` is a value type, we map `v.a` to the equivalent of:
51+
- If `U` is a value type, we map `v.a` to:
4452
```scala
45-
v.a
46-
--->
47-
(v: Selectable).selectDynamic("a").asInstanceOf[U]
53+
v.selectDynamic("a").asInstanceOf[U]
4854
```
4955

50-
- If `U` is a method type `(T11, ..., T1n)...(TN1, ..., TNn) => R` and it is not
51-
a dependent method type, we map `v.a(a11, ..., a1n)...(aN1, aNn)` to
52-
the equivalent of:
56+
- If `U` is a method type `(T11, ..., T1n)...(TN1, ..., TNn): R` and it is not a dependent method type, we map `v.a(a11, ..., a1n)...(aN1, ..., aNn)` to:
57+
```scala
58+
v.applyDynamic("a")(a11, ..., a1n, ..., aN1, ..., aNn)
59+
.asInstanceOf[R]
60+
```
61+
If this call resolves to an `applyDynamic` method of the second form that takes a `ClassTag[?]*` argument, we further rewrite this call to
5362
```scala
54-
v.a(arg1, ..., argn)
55-
--->
56-
(v: Selectable).applyDynamic("a", CT11, ..., CTn, ..., CTN1, ... CTNn)
57-
(a11, ..., a1n, ..., aN1, ..., aNn)
58-
.asInstanceOf[R]
63+
v.applyDynamic("a", CT11, ..., CT1n, ..., CTN1, ... CTNn)(
64+
a11, ..., a1n, ..., aN1, ..., aNn)
65+
.asInstanceOf[R]
5966
```
67+
where each `CT_ij` is the class tag of the type of the formal parameter `Tij`
6068

6169
- If `U` is neither a value nor a method type, or a dependent method
6270
type, an error is emitted.
6371

64-
We make sure that `r` conforms to type `Selectable`, potentially by
65-
introducing an implicit conversion, and then call either
66-
`selectDynamic` or `applyDynamic`, passing the name of the
67-
member to access, along with the class tags of the formal parameters
68-
and the arguments in the case of a method call. These parameters
69-
could be used to disambiguate one of several overload variants in the
70-
future, but overloads are not supported in structural types at the
71-
moment.
72+
Note that `v`'s static type does not necessarily have to conform to `Selectable`, nor does it need to have `selectDynamic` and `applyDynamic` as members. It suffices that there is an implicit
73+
conversion that can turn `v` into a `Selectable`, and the selection methods could also be available as extension methods.
7274

7375
## Limitations of structural types
7476

@@ -85,14 +87,8 @@ moment.
8587
- In Scala 2, mutable `var`s are allowed in refinements. In Scala 3,
8688
they are no longer allowed.
8789

88-
## Migration
89-
90-
Receivers of structural calls need to be instances of `Selectable`. A
91-
conversion from `Any` to `Selectable` is available in the standard
92-
library, in `scala.reflect.Selectable.reflectiveSelectable`. This is
93-
similar to the implementation of structural types in Scala 2.
9490

95-
## Reference
91+
## Context
9692

9793
For more info, see [Rethink Structural
9894
Types](https://github.com/lampepfl/dotty/issues/1886).

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

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ layout: doc-page
33
title: "Programmatic Structural Types"
44
---
55

6+
## Motivation
7+
68
Some usecases, such as modelling database access, are more awkward in
79
statically typed languages than in dynamically typed languages: With
810
dynamically typed languages, it's quite natural to model a row as a
@@ -29,25 +31,91 @@ configure how fields and methods should be resolved.
2931

3032
## Example
3133

34+
Here's an example of a structural type `Person`:
3235
```scala
33-
object StructuralTypeExample {
34-
35-
case class Record(elems: (String, Any)*) extends Selectable {
36-
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
36+
class Record(elems: (String, Any)*) extends Selectable {
37+
private val fields = elems.toMap
38+
def selectDynamic(name: String): Any = fields(name)
3739
}
38-
3940
type Person = Record {
4041
val name: String
4142
val age: Int
4243
}
44+
```
45+
The person type adds a _refinement_ to its parent type `Record` that defines `name` and `age` fields. We say the refinement is _structural_ since `name` and `age` are not defined in the parent type. But they exist nevertheless as members of class `Person`. For instance, the following
46+
program would print "Emma is 42 years old.":
47+
```scala
48+
val person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person]
49+
println(s"${person.name} is ${person.age} years old.")
50+
```
51+
The parent type `Record` in this example is a generic class that can represent arbitrary records in its `elems` argument. This argument is a
52+
sequence of pairs of labels of type `String` and values of type `Any`.
53+
When we create a `Person` as a `Record` we have to assert with a typecast
54+
that the record defines the right fields of the right types. `Record`
55+
itself is too weakly typed so the compiler cannot know this without
56+
help from the user. In practice, the connection between a structural type
57+
and its underlying generic representation would most likely be done by
58+
a database layer, and therefore would not be a concern of the end user.
59+
60+
`Record` extends the marker trait `scala.Selectable` and defines
61+
a method `selectDynamic`, which maps a field name to its value.
62+
Selecting a structural type member is done by calling this method.
63+
The `person.name` and `person.age` selections are translated by
64+
the Scala compiler to:
65+
```scala
66+
person.selectDynamic("name").asInstanceOf[String]
67+
person.selectDynamic("age").asInstanceOf[Int]
68+
```
69+
70+
Besides `selectDynamic`, a `Selectable` class sometimes also defines a method `applyDynamic`. This can then be used to translate function calls of structural members. So, if `a` is an instance of `Selectable`, a structural call like `a.f(b, c)` would translate to
71+
```scala
72+
a.applyDynamic("f")(b, c)
73+
```
74+
75+
## Using Java Reflection
4376

44-
def main(args: Array[String]): Unit = {
45-
val person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person]
46-
println(s"${person.name} is ${person.age} years old.")
47-
// Prints: Emma is 42 years old.
77+
Structural types can also be accessed using Java reflection. Example:
78+
```scala
79+
type Closeable = {
80+
def close(): Unit
81+
}
82+
class FileInputStream {
83+
def close(): Unit
84+
}
85+
class Channel {
86+
def close(): Unit
4887
}
49-
}
5088
```
89+
Here, we define a structural type `Closeable` that defines a `close` method. There are various classes that have `close` methods, we just list `FileInputStream` and `Channel` as two examples. It would be easiest if the two classes shared a common interface that factors out the `close` method. But such factorings are often not possible if different libraries are combined in one application. Yet, we can still have methods that work on
90+
all classes with a `close` method by using the `Closeable` type. For instance,
91+
```scala
92+
import scala.reflect.Selectable.reflectiveSelectable
93+
94+
def autoClose(f: Closeable)(op: Closeable => Unit): Unit =
95+
try op(f) finally f.close()
96+
```
97+
The call `f.close()` has to use Java reflection to identify and call the `close` method in the receiver `f`. This needs to be enabled by an import
98+
of `reflectiveSelectable` shown above. What happens "under the hood" is then the following:
99+
100+
- The import makes available an implicit conversion that turns any type into a
101+
`Selectable`. `f` is wrapped in this conversion.
102+
103+
- The compiler then transforms the `close` call on the wrapped `f`
104+
to an `applyDynamic` call. The end result is:
105+
106+
```scala
107+
reflectiveSelectable(f).applyDynamic("close")()
108+
```
109+
- The implementation of `applyDynamic` in `reflectiveSelectable`'s result
110+
uses Java reflection to find and call a method `close` with zero parameters in the value referenced by `f` at runtime.
111+
112+
Structural calls like this tend to be much slower than normal method calls. The mandatory import of `reflectiveSelectable` serves as a signpost that something inefficient is going on.
113+
114+
**Note:** In Scala 2, Java reflection is the only mechanism available for structural types and it is automatically enabled without needing the
115+
`reflectiveSelectable` conversion. However, to warn against inefficient
116+
dispatch, Scala 2 requires a language import `import scala.language.reflectiveCalls`.
117+
118+
Before resorting to structural calls with Java reflection one should consider alternatives. For instance, sometimes a more a modular _and_ efficient architecture can be obtained using typeclasses.
51119

52120
## Extensibility
53121

library/src/scala/Selectable.scala

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
package scala
22
import scala.reflect.ClassTag
33

4-
trait Selectable extends Any {
5-
def selectDynamic(name: String): Any
6-
def applyDynamic(name: String, paramClasses: ClassTag[_]*)(args: Any*): Any =
7-
new UnsupportedOperationException("applyDynamic")
8-
}
4+
/** A marker trait for objects that support structural selection via
5+
* `selectDynamic` and `applyDynamic`
6+
*
7+
* Implementation classes should define, or make available as extension
8+
* methods, the following two method signatures:
9+
*
10+
* def selectDynamic(name: String): Any
11+
* def applyDynamic(name: String)(args: Any*): Any =
12+
*
13+
* `selectDynamic` is invoked for simple selections `v.m`, whereas
14+
* `applyDynamic` is invoked for selections with arguments `v.m(...)`.
15+
* If there's only one kind of selection, the method supporting the
16+
* other may be omitted. The `applyDynamic` can also have a second parameter
17+
* list of class tag arguments, i.e. it may alternatively have the signature
18+
*
19+
* def applyDynamic(name: String, paramClasses: ClassTag[_]*)(args: Any*): Any
20+
*
21+
* In this case the call will synthesize `ClassTag` arguments for all formal parameter
22+
* types of the method in the structural type.
23+
*/
24+
trait Selectable extends Any

library/src/scala/reflect/Selectable.scala

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Selectable(val receiver: Any) extends AnyVal with scala.Selectable {
1414
}
1515
}
1616

17-
override def applyDynamic(name: String, paramTypes: ClassTag[_]*)(args: Any*): Any = {
17+
def applyDynamic(name: String, paramTypes: ClassTag[_]*)(args: Any*): Any = {
1818
val rcls = receiver.getClass
1919
val paramClasses = paramTypes.map(_.runtimeClass)
2020
val mth = rcls.getMethod(name, paramClasses: _*)
@@ -24,8 +24,6 @@ class Selectable(val receiver: Any) extends AnyVal with scala.Selectable {
2424
}
2525

2626
object Selectable {
27-
implicit def reflectiveSelectable(receiver: Any): scala.Selectable = receiver match {
28-
case receiver: scala.Selectable => receiver
29-
case _ => new Selectable(receiver)
30-
}
27+
implicit def reflectiveSelectable(receiver: Any): Selectable =
28+
new Selectable(receiver)
3129
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
type Closeable = {
2+
def close(): Unit
3+
}
4+
5+
class FileInputStream:
6+
def close(): Unit = ()
7+
8+
class Channel:
9+
def close(): Unit = ()
10+
11+
import scala.reflect.Selectable.reflectiveSelectable
12+
13+
def autoClose(f: Closeable)(op: Closeable => Unit): Unit =
14+
try op(f) finally f.close()

tests/run-macros/refined-selectable-macro/Macro_1.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import scala.quoted._
22

33
object Macro {
44

5+
trait Selectable extends scala.Selectable:
6+
def selectDynamic(name: String): Any
7+
58
trait SelectableRecord extends Selectable {
69
transparent inline def toTuple: Tuple = ${ toTupleImpl('this)}
710
}

0 commit comments

Comments
 (0)