Skip to content

More details about structural types #5341

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
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ public enum ErrorMessageID {
PureExpressionInStatementPositionID,
TraitCompanionWithMutableStaticID,
LazyStaticFieldID,
StaticOverridingNonStaticMembersID
StaticOverridingNonStaticMembersID,
OverloadInRefinementID
;

public int errorNumber() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2170,4 +2170,13 @@ object messages {
override def kind: String = "Syntax"
override def explanation: String = ""
}

case class OverloadInRefinement(rsym: Symbol)(implicit val ctx: Context)
extends Message(OverloadInRefinementID) {
override def msg: String = "Refinements cannot introduce overloaded definitions"
override def kind: String = "Overload"
override def explanation: String =
hl"""The refinement `$rsym` introduces an overloaded definition.
|Refinements cannot contain overloaded definitions.""".stripMargin
}
}
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Dynamic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ trait Dynamic { self: Typer with Applications =>
*
* If `U` is a value type, map `x.a` to the equivalent of:
*
* (x: Selectable).selectDynamic(x, "a").asInstanceOf[U]
* (x: Selectable).selectDynamic("a").asInstanceOf[U]
*
* If `U` is a method type (T1,...,Tn)R, map `x.a` to the equivalent of:
*
Expand Down
5 changes: 5 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,11 @@ class Typer extends Namer
val rsym = refinement.symbol
if (rsym.info.isInstanceOf[PolyType] && rsym.allOverriddenSymbols.isEmpty)
ctx.error(PolymorphicMethodMissingTypeInParent(rsym, tpt1.symbol), refinement.pos)

val member = refineCls.info.member(rsym.name)
if (member.isOverloaded) {
ctx.error(OverloadInRefinement(rsym), refinement.pos)
}
}
assignType(cpy.RefinedTypeTree(tree)(tpt1, refinements1), tpt1, refinements1, refineCls)
}
Expand Down
97 changes: 97 additions & 0 deletions docs/docs/reference/changed/structural-types-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
layout: doc-page
title: "Programmatic Structural Types - More Details"
---

## Syntax

```
SimpleType ::= ... | Refinement
Refinement ::= ‘{’ RefineStatSeq ‘}’
RefineStatSeq ::= RefineStat {semi RefineStat}
RefineStat ::= ‘val’ VarDcl | ‘def’ DefDcl | ‘type’ {nl} TypeDcl
```

## Implementation of structural types

The standard library defines a trait `Selectable` in the package
`scala`, defined as follows:

```scala
trait Selectable extends Any {
def selectDynamic(name: String): Any
def selectDynamicMethod(name: String, paramClasses: ClassTag[_]*): Any =
new UnsupportedOperationException("selectDynamicMethod")
}
```

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

`selectDynamic` takes a field name and returns the value associated
with that name in the `Selectable`. Similarly, `selectDynamicMethod`
takes a method name, `ClassTag`s representing its parameters types and
will return the function that matches this
name and parameter types.

Given a value `v` of type `C { Rs }`, where `C` is a class reference
and `Rs` are refinement declarations, and given `v.a` of type `U`, we
consider three distinct cases:

- If `U` is a value type, we map `v.a` to the equivalent of:
```scala
v.a
--->
(v: Selectable).selectDynamic("a").asInstanceOf[U]
```

- If `U` is a method type `(T1, ..., Tn) => R` with at most 7
parameters and it is not a dependent method type, we map `v.a` to
the equivalent of:
```scala
v.a
--->
(v: Selectable).selectDynamic("a", CT1, ..., CTn).asInstanceOf[(T1, ..., Tn) => R]
```

- If `U` is neither a value nor a method type, or a dependent method
type, or has more than 7 parameters, an error is emitted.

We make sure that `r` conforms to type `Selectable`, potentially by
introducing an implicit conversion, and then call either
`selectDynamic` or `selectMethodDynamic`, passing the name of the
member to access and the class tags of the formal parameters, in the
case of a method call. These parameters could be used to disambiguate
one of several overload variants in the future, but overloads are not
supported in structural types at the moment.

## Limitations of structural types

- Methods with more than 7 formal parameters cannot be called via
structural call.
- Dependent methods cannot be called via structural call.
- Overloaded methods cannot be called via structural call.
- Refinements do not handle polymorphic methods.

## Differences with Scala 2 structural types

- Scala 2 supports structural types by means of Java reflection. Unlike
Scala 3, structural calls do not rely on a mechanism such as
`Selectable`, and reflection cannot be avoided.
- In Scala 2, structural calls to overloaded methods are possible.
- In Scala 2, mutable `var`s are allowed in refinements. In Scala 3,
they are no longer allowed.

## Migration

Receivers of structural calls need to be instances of `Selectable`. A
conversion from `Any` to `Selectable` is available in the standard
library, in `scala.reflect.Selectable.reflectiveSelectable`. This is
similar to the implementation of structural types in Scala 2.

## Reference

For more info, see [Rethink Structural
Types](https://github.com/lampepfl/dotty/issues/1886).
180 changes: 63 additions & 117 deletions docs/docs/reference/changed/structural-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,131 +3,77 @@ layout: doc-page
title: "Programmatic Structural Types"
---

Previously, Scala supported structural types by means of
reflection. This is problematic on other platforms, because Scala's
reflection is JVM-based. Consequently, Scala.js and Scala.native don't
support structural types fully. The reflection based implementation is
also needlessly restrictive, since it rules out other implementation
schemes. This makes structural types unsuitable for e.g. modelling
rows in a database, for which they would otherwise seem to be an ideal
match.

Dotty allows to implement structural types programmatically, using
"Selectables". `Selectable` is a trait defined as follows:

trait Selectable extends Any {
def selectDynamic(name: String): Any
def selectDynamicMethod(name: String, paramClasses: ClassTag[_]*): Any =
new UnsupportedOperationException("selectDynamicMethod")
}

The most important method of a `Selectable` is `selectDynamic`: It
takes a field name and returns the value associated with that name in
the selectable.

Assume now `r` is a value with structural type `S`. In general `S` is
of the form `C { Rs }`, i.e. it consists of a class reference `C` and
refinement declarations `Rs`. We call a field selection `r.f`
_structural_ if `f` is a name defined by a declaration in `Rs` whereas
`C` defines no member of name `f`. Assuming the selection has type
`T`, it is mapped to something equivalent to the following code:

(r: Selectable).selectDynamic("f").asInstanceOf[T]

That is, we make sure `r` conforms to type `Selectable`, potentially
by adding an implicit conversion. We then invoke the `get` operation
of that instance, passing the the name `"f"` as a parameter. We
finally cast the resulting value back to the statically known type
`T`.

`Selectable` also defines another access method called
`selectDynamicMethod`. This operation is used to select methods
instead of fields. It gets passed the class tags of the selected
method's formal parameter types as additional arguments. These can
then be used to disambiguate one of several overloaded variants.

Package `scala.reflect` contains an implicit conversion which can map
any value to a selectable that emulates reflection-based selection, in
a way similar to what was done until now:

package scala.reflect

object Selectable {
implicit def reflectiveSelectable(receiver: Any): scala.Selectable =
receiver match {
case receiver: scala.Selectable => receiver
case _ => new scala.reflect.Selectable(receiver)
}
}

When imported, `reflectiveSelectable` provides a way to access fields
of any structural type using Java reflection. This is similar to the
current implementation of structural types. The main difference is
that to get reflection-based structural access one now has to add an
import:

import scala.reflect.Selectable.reflectiveSelectable

On the other hand, the previously required language feature import of
`reflectiveCalls` is now redundant and is therefore dropped.

As you can see from its implementation above, `reflectSelectable`
checks first whether its argument is already a run-time instance of
`Selectable`, in which case it is returned directly. This means that
reflection-based accesses only take place as a last resort, if no
other `Selectable` is defined.

Other selectable instances can be defined in libraries. For instance,
here is a simple class of records that support dynamic selection:

case class Record(elems: (String, Any)*) extends Selectable {
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
}

`Record` consists of a list of pairs of element names and values. Its
`selectDynamic` operation finds the pair with given name and returns
its value.

For illustration, let's define a record value and cast it to a
structural type `Person`:

type Person = Record { val name: String; val age: Int }
Some usecases, such as modelling database access, are more awkward in
statically typed languages than in dynamically typed languages: With
dynamically typed languages, it's quite natural to model a row as a
record or object, and to select entries with simple dot notation (e.g.
`row.columnName`).

Achieving the same experience in statically typed
language requires defining a class for every possible row arising from
database manipulation (including rows arising from joins and
projections) and setting up a scheme to map between a row and the
class representing it.

This requires a large amount of boilerplate, which leads developers to
trade the advantages of static typing for simpler schemes where colum
names are represented as strings and passed to other operators (e.g.
`row.select("columnName")`). This approach forgoes the advantages of
static typing, and is still not as natural as the dynamically typed
version.

Structural types help in situations where we would like to support
simple dot notation in dynamic contexts without losing the advantages
of static typing. They allow developers to use dot notation and
configure how fields and methods should be resolved.

## Example

```scala
object StructuralTypeExample {

case class Record(elems: (String, Any)*) extends Selectable {
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
}

type Person = Record {
val name: String
val age: Int
}

def main(args: Array[String]): Unit = {
val person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person]
println(s"${person.name} is ${person.age} years old.")
// Prints: Emma is 42 years old.
}
}
```

Then `person.name` will have static type `String`, and will produce `"Emma"` as result.
## Extensibility

The safety of this scheme relies on the correctness of the cast. If
the cast lies about the structure of the record, the corresponding
`selectDynamic` operation would fail. In practice, the cast would
likely be part if a database access layer which would ensure its
correctness.
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.

### Notes:
## Relation with `scala.Dynamic`

1. The scheme does not handle polymorphic methods in structural
refinements. Such polymorphic methods are currently flagged as
errors. It's not clear whether the use case is common enough to
warrant the additional complexity of supporting it.

2. There are clearly some connections with `scala.Dynamic` here, since
There are clearly some connections with `scala.Dynamic` here, since
both select members programmatically. But there are also some
differences.

- Fully dynamic selection is not typesafe, but structural selection
is, as long as the correspondence of the structural type with the
underlying value is as stated.

- `Dynamic` is just a marker trait, which gives more leeway where and
how to define reflective access operations. By contrast
`Selectable` is a trait which declares the access operations.
- Fully dynamic selection is not typesafe, but structural selection
is, as long as the correspondence of the structural type with the
underlying value is as stated.

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

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

For more info, see [Issue #1886](https://github.com/lampepfl/dotty/issues/1886).
[More details](structural-types-spec.html)
8 changes: 8 additions & 0 deletions tests/neg/structural.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,12 @@ object Test3 {

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

type A = { def foo(x: Int): Unit; def foo(x: String): Unit } // error: overloaded definition // error: overloaded definition
type B = { val foo: Int; def foo: Int } // error: duplicate foo

type C = { var foo: Int } // error: refinements cannot have vars

trait Entry { type Key; val key: Key }
type D = { def foo(e: Entry, k: e.Key): Unit }
def i(x: D) = x.foo(???) // error: foo has dependent params
}