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 universal marker trait Selectable in the package scala:
trait Selectable extends Any
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.
Implementations of Selectable have to make available one or both of
the methods selectDynamic and applyDynamic. The methods could be members of the Selectable implementation or they could be extension methods.
The selectDynamic method takes a field name and returns the value associated with that name in the Selectable.
It should have a signature of the form:
def selectDynamic(name: String): T
Often, the return type T is Any.
The applyDynamic method is used for selections that are applied to arguments. It takes a method name and possibly ClassTags representing its parameters types as well as the arguments to pass to the function.
Its signature should be of one of the two following forms:
def applyDynamic(name: String)(args: Any*): T
def applyDynamic(name: String, ctags: ClassTag[?]*)(args: Any*): T
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
if applyDynamic is implemented using Java reflection, but it could be
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.
Given a value v of type C { Rs }, where C is a class reference
and Rs are structural refinement declarations, and given v.a of type U, we consider three distinct cases:
- If
Uis a value type, we mapv.ato:v.selectDynamic("a").asInstanceOf[U] - If
Uis a method type(T11, ..., T1n)...(TN1, ..., TNn): Rand it is not a dependent method type, we mapv.a(a11, ..., a1n)...(aN1, ..., aNn)to:v.applyDynamic("a")(a11, ..., a1n, ..., aN1, ..., aNn) .asInstanceOf[R]If this call resolves to an
applyDynamicmethod of the second form that takes aClassTag[?]*argument, we further rewrite this call tov.applyDynamic("a", CT11, ..., CT1n, ..., CTN1, ... CTNn)( a11, ..., a1n, ..., aN1, ..., aNn) .asInstanceOf[R]where each
CT_ijis the class tag of the type of the formal parameterTij - If
Uis neither a value nor a method type, or a dependent method type, an error is emitted.
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
conversion that can turn v into a Selectable, and the selection methods could also be available as extension methods.
Limitations of structural types
- 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
vars are allowed in refinements. In Scala 3, they are no longer allowed.
Context
For more info, see Rethink Structural Types.