Skip to content

Commit ce0acf1

Browse files
authored
Merge pull request #5341 from dotty-staging/details-programmatic-structural-types
More details about structural types
2 parents 7c7f687 + be977d9 commit ce0acf1

File tree

7 files changed

+185
-119
lines changed

7 files changed

+185
-119
lines changed

compiler/src/dotty/tools/dotc/reporting/diagnostic/ErrorMessageID.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ public enum ErrorMessageID {
139139
PureExpressionInStatementPositionID,
140140
TraitCompanionWithMutableStaticID,
141141
LazyStaticFieldID,
142-
StaticOverridingNonStaticMembersID
142+
StaticOverridingNonStaticMembersID,
143+
OverloadInRefinementID
143144
;
144145

145146
public int errorNumber() {

compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2170,4 +2170,13 @@ object messages {
21702170
override def kind: String = "Syntax"
21712171
override def explanation: String = ""
21722172
}
2173+
2174+
case class OverloadInRefinement(rsym: Symbol)(implicit val ctx: Context)
2175+
extends Message(OverloadInRefinementID) {
2176+
override def msg: String = "Refinements cannot introduce overloaded definitions"
2177+
override def kind: String = "Overload"
2178+
override def explanation: String =
2179+
hl"""The refinement `$rsym` introduces an overloaded definition.
2180+
|Refinements cannot contain overloaded definitions.""".stripMargin
2181+
}
21732182
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ trait Dynamic { self: Typer with Applications =>
117117
*
118118
* If `U` is a value type, map `x.a` to the equivalent of:
119119
*
120-
* (x: Selectable).selectDynamic(x, "a").asInstanceOf[U]
120+
* (x: Selectable).selectDynamic("a").asInstanceOf[U]
121121
*
122122
* If `U` is a method type (T1,...,Tn)R, map `x.a` to the equivalent of:
123123
*

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,11 @@ class Typer extends Namer
12351235
val rsym = refinement.symbol
12361236
if (rsym.info.isInstanceOf[PolyType] && rsym.allOverriddenSymbols.isEmpty)
12371237
ctx.error(PolymorphicMethodMissingTypeInParent(rsym, tpt1.symbol), refinement.pos)
1238+
1239+
val member = refineCls.info.member(rsym.name)
1240+
if (member.isOverloaded) {
1241+
ctx.error(OverloadInRefinement(rsym), refinement.pos)
1242+
}
12381243
}
12391244
assignType(cpy.RefinedTypeTree(tree)(tpt1, refinements1), tpt1, refinements1, refineCls)
12401245
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
layout: doc-page
3+
title: "Programmatic Structural Types - More Details"
4+
---
5+
6+
## Syntax
7+
8+
```
9+
SimpleType ::= ... | Refinement
10+
Refinement ::= ‘{’ RefineStatSeq ‘}’
11+
RefineStatSeq ::= RefineStat {semi RefineStat}
12+
RefineStat ::= ‘val’ VarDcl | ‘def’ DefDcl | ‘type’ {nl} TypeDcl
13+
```
14+
15+
## Implementation of structural types
16+
17+
The standard library defines a trait `Selectable` in the package
18+
`scala`, defined as follows:
19+
20+
```scala
21+
trait Selectable extends Any {
22+
def selectDynamic(name: String): Any
23+
def selectDynamicMethod(name: String, paramClasses: ClassTag[_]*): Any =
24+
new UnsupportedOperationException("selectDynamicMethod")
25+
}
26+
```
27+
28+
An implementation of `Selectable` that relies on Java reflection is
29+
available in the standard library: `scala.reflect.Selectable`. Other
30+
implementations can be envisioned for platforms where Java reflection
31+
is not available.
32+
33+
`selectDynamic` takes a field name and returns the value associated
34+
with that name in the `Selectable`. Similarly, `selectDynamicMethod`
35+
takes a method name, `ClassTag`s representing its parameters types and
36+
will return the function that matches this
37+
name and parameter types.
38+
39+
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:
42+
43+
- If `U` is a value type, we map `v.a` to the equivalent of:
44+
```scala
45+
v.a
46+
--->
47+
(v: Selectable).selectDynamic("a").asInstanceOf[U]
48+
```
49+
50+
- If `U` is a method type `(T1, ..., Tn) => R` with at most 7
51+
parameters and it is not a dependent method type, we map `v.a` to
52+
the equivalent of:
53+
```scala
54+
v.a
55+
--->
56+
(v: Selectable).selectDynamic("a", CT1, ..., CTn).asInstanceOf[(T1, ..., Tn) => R]
57+
```
58+
59+
- If `U` is neither a value nor a method type, or a dependent method
60+
type, or has more than 7 parameters, an error is emitted.
61+
62+
We make sure that `r` conforms to type `Selectable`, potentially by
63+
introducing an implicit conversion, and then call either
64+
`selectDynamic` or `selectMethodDynamic`, passing the name of the
65+
member to access and the class tags of the formal parameters, in the
66+
case of a method call. These parameters could be used to disambiguate
67+
one of several overload variants in the future, but overloads are not
68+
supported in structural types at the moment.
69+
70+
## Limitations of structural types
71+
72+
- Methods with more than 7 formal parameters cannot be called via
73+
structural call.
74+
- Dependent methods cannot be called via structural call.
75+
- Overloaded methods cannot be called via structural call.
76+
- Refinements do not handle polymorphic methods.
77+
78+
## Differences with Scala 2 structural types
79+
80+
- Scala 2 supports structural types by means of Java reflection. Unlike
81+
Scala 3, structural calls do not rely on a mechanism such as
82+
`Selectable`, and reflection cannot be avoided.
83+
- In Scala 2, structural calls to overloaded methods are possible.
84+
- In Scala 2, mutable `var`s are allowed in refinements. In Scala 3,
85+
they are no longer allowed.
86+
87+
## Migration
88+
89+
Receivers of structural calls need to be instances of `Selectable`. A
90+
conversion from `Any` to `Selectable` is available in the standard
91+
library, in `scala.reflect.Selectable.reflectiveSelectable`. This is
92+
similar to the implementation of structural types in Scala 2.
93+
94+
## Reference
95+
96+
For more info, see [Rethink Structural
97+
Types](https://github.com/lampepfl/dotty/issues/1886).

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

Lines changed: 63 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -3,131 +3,77 @@ layout: doc-page
33
title: "Programmatic Structural Types"
44
---
55

6-
Previously, Scala supported structural types by means of
7-
reflection. This is problematic on other platforms, because Scala's
8-
reflection is JVM-based. Consequently, Scala.js and Scala.native don't
9-
support structural types fully. The reflection based implementation is
10-
also needlessly restrictive, since it rules out other implementation
11-
schemes. This makes structural types unsuitable for e.g. modelling
12-
rows in a database, for which they would otherwise seem to be an ideal
13-
match.
14-
15-
Dotty allows to implement structural types programmatically, using
16-
"Selectables". `Selectable` is a trait defined as follows:
17-
18-
trait Selectable extends Any {
19-
def selectDynamic(name: String): Any
20-
def selectDynamicMethod(name: String, paramClasses: ClassTag[_]*): Any =
21-
new UnsupportedOperationException("selectDynamicMethod")
22-
}
23-
24-
The most important method of a `Selectable` is `selectDynamic`: It
25-
takes a field name and returns the value associated with that name in
26-
the selectable.
27-
28-
Assume now `r` is a value with structural type `S`. In general `S` is
29-
of the form `C { Rs }`, i.e. it consists of a class reference `C` and
30-
refinement declarations `Rs`. We call a field selection `r.f`
31-
_structural_ if `f` is a name defined by a declaration in `Rs` whereas
32-
`C` defines no member of name `f`. Assuming the selection has type
33-
`T`, it is mapped to something equivalent to the following code:
34-
35-
(r: Selectable).selectDynamic("f").asInstanceOf[T]
36-
37-
That is, we make sure `r` conforms to type `Selectable`, potentially
38-
by adding an implicit conversion. We then invoke the `get` operation
39-
of that instance, passing the the name `"f"` as a parameter. We
40-
finally cast the resulting value back to the statically known type
41-
`T`.
42-
43-
`Selectable` also defines another access method called
44-
`selectDynamicMethod`. This operation is used to select methods
45-
instead of fields. It gets passed the class tags of the selected
46-
method's formal parameter types as additional arguments. These can
47-
then be used to disambiguate one of several overloaded variants.
48-
49-
Package `scala.reflect` contains an implicit conversion which can map
50-
any value to a selectable that emulates reflection-based selection, in
51-
a way similar to what was done until now:
52-
53-
package scala.reflect
54-
55-
object Selectable {
56-
implicit def reflectiveSelectable(receiver: Any): scala.Selectable =
57-
receiver match {
58-
case receiver: scala.Selectable => receiver
59-
case _ => new scala.reflect.Selectable(receiver)
60-
}
61-
}
62-
63-
When imported, `reflectiveSelectable` provides a way to access fields
64-
of any structural type using Java reflection. This is similar to the
65-
current implementation of structural types. The main difference is
66-
that to get reflection-based structural access one now has to add an
67-
import:
68-
69-
import scala.reflect.Selectable.reflectiveSelectable
70-
71-
On the other hand, the previously required language feature import of
72-
`reflectiveCalls` is now redundant and is therefore dropped.
73-
74-
As you can see from its implementation above, `reflectSelectable`
75-
checks first whether its argument is already a run-time instance of
76-
`Selectable`, in which case it is returned directly. This means that
77-
reflection-based accesses only take place as a last resort, if no
78-
other `Selectable` is defined.
79-
80-
Other selectable instances can be defined in libraries. For instance,
81-
here is a simple class of records that support dynamic selection:
82-
83-
case class Record(elems: (String, Any)*) extends Selectable {
84-
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
85-
}
86-
87-
`Record` consists of a list of pairs of element names and values. Its
88-
`selectDynamic` operation finds the pair with given name and returns
89-
its value.
90-
91-
For illustration, let's define a record value and cast it to a
92-
structural type `Person`:
93-
94-
type Person = Record { val name: String; val age: Int }
6+
Some usecases, such as modelling database access, are more awkward in
7+
statically typed languages than in dynamically typed languages: With
8+
dynamically typed languages, it's quite natural to model a row as a
9+
record or object, and to select entries with simple dot notation (e.g.
10+
`row.columnName`).
11+
12+
Achieving the same experience in statically typed
13+
language requires defining a class for every possible row arising from
14+
database manipulation (including rows arising from joins and
15+
projections) and setting up a scheme to map between a row and the
16+
class representing it.
17+
18+
This requires a large amount of boilerplate, which leads developers to
19+
trade the advantages of static typing for simpler schemes where colum
20+
names are represented as strings and passed to other operators (e.g.
21+
`row.select("columnName")`). This approach forgoes the advantages of
22+
static typing, and is still not as natural as the dynamically typed
23+
version.
24+
25+
Structural types help in situations where we would like to support
26+
simple dot notation in dynamic contexts without losing the advantages
27+
of static typing. They allow developers to use dot notation and
28+
configure how fields and methods should be resolved.
29+
30+
## Example
31+
32+
```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
37+
}
38+
39+
type Person = Record {
40+
val name: String
41+
val age: Int
42+
}
43+
44+
def main(args: Array[String]): Unit = {
9545
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.
48+
}
49+
}
50+
```
9651

97-
Then `person.name` will have static type `String`, and will produce `"Emma"` as result.
52+
## Extensibility
9853

99-
The safety of this scheme relies on the correctness of the cast. If
100-
the cast lies about the structure of the record, the corresponding
101-
`selectDynamic` operation would fail. In practice, the cast would
102-
likely be part if a database access layer which would ensure its
103-
correctness.
54+
New instances of `Selectable` can be defined to support means of
55+
access other than Java reflection, which would enable usages such as
56+
the database access example given at the beginning of this document.
10457

105-
### Notes:
58+
## Relation with `scala.Dynamic`
10659

107-
1. The scheme does not handle polymorphic methods in structural
108-
refinements. Such polymorphic methods are currently flagged as
109-
errors. It's not clear whether the use case is common enough to
110-
warrant the additional complexity of supporting it.
111-
112-
2. There are clearly some connections with `scala.Dynamic` here, since
60+
There are clearly some connections with `scala.Dynamic` here, since
11361
both select members programmatically. But there are also some
11462
differences.
11563

116-
- Fully dynamic selection is not typesafe, but structural selection
117-
is, as long as the correspondence of the structural type with the
118-
underlying value is as stated.
119-
120-
- `Dynamic` is just a marker trait, which gives more leeway where and
121-
how to define reflective access operations. By contrast
122-
`Selectable` is a trait which declares the access operations.
64+
- Fully dynamic selection is not typesafe, but structural selection
65+
is, as long as the correspondence of the structural type with the
66+
underlying value is as stated.
12367

124-
- One access operation, `selectDynamic` is shared between both
125-
approaches, but the other access operations are
126-
different. `Selectable` defines a `selectDynamicMethod`, which
127-
takes class tags indicating the method's formal parameter types as
128-
additional argument. `Dynamic` comes with `applyDynamic` and
129-
`updateDynamic` methods, which take actual argument values.
68+
- `Dynamic` is just a marker trait, which gives more leeway where and
69+
how to define reflective access operations. By contrast
70+
`Selectable` is a trait which declares the access operations.
13071

131-
### Reference
72+
- One access operation, `selectDynamic` is shared between both
73+
approaches, but the other access operations are
74+
different. `Selectable` defines a `selectDynamicMethod`, which
75+
takes class tags indicating the method's formal parameter types as
76+
additional argument. `Dynamic` comes with `applyDynamic` and
77+
`updateDynamic` methods, which take actual argument values.
13278

133-
For more info, see [Issue #1886](https://github.com/lampepfl/dotty/issues/1886).
79+
[More details](structural-types-spec.html)

tests/neg/structural.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,12 @@ object Test3 {
88

99
def h(x: { def f[T](a: T): Int }) = x.f[Int](4) // error: polymorphic refinement method ... no longer allowed
1010

11+
type A = { def foo(x: Int): Unit; def foo(x: String): Unit } // error: overloaded definition // error: overloaded definition
12+
type B = { val foo: Int; def foo: Int } // error: duplicate foo
13+
14+
type C = { var foo: Int } // error: refinements cannot have vars
15+
16+
trait Entry { type Key; val key: Key }
17+
type D = { def foo(e: Entry, k: e.Key): Unit }
18+
def i(x: D) = x.foo(???) // error: foo has dependent params
1119
}

0 commit comments

Comments
 (0)