Structural Types, Selectable & Dynamic
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala
3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). Where an example was also compiled with the 3.9 LTS line (3.9.0)
and the 3.3 LTS line (3.3.8), the text says what happened. Most Scala code is typed nominally: a method that needs
something with a name takes a trait or class that declares it. A structural type describes only the members a value
must have, whatever its class, so { def name: String } accepts a Hut, a Trail and an anonymous object alike. Scala 3
also lets a library decide how such members are looked up, through Selectable, and keeps Dynamic for member names that
are not known to the compiler at all. The examples use the trailhead scenario, a toolkit that plans hikes on a network of
mountain trails. For the nominal alternative see Traits & Mixins
and Generics & Bounds; for the other ways of combining types
see Union & Intersection Types; for named tuples, which
the last section combines with Selectable, see
Tuples & Named Tuples.
Refinements and structural types
A refinement adds members to a type: Named { def name: String } is "a Named that also has name`". Written on its own, as
in `{ def name: String }, it refines AnyRef and is a structural type because only the shape is stated. A type alias
gives it a name. A structural type can contain def, val and type members.
Calling a member that the static type describes but the runtime class does not statically declare needs a way to find it.
Scala 3 does not guess; the receiver has to be Selectable or Dynamic. The standard library supplies a ready-made
implicit conversion, scala.reflect.Selectable.reflectiveSelectable, which finds the member with Java reflection. The
example passes a class, a val member, a method with a parameter and an anonymous object to structural types.
//> using scala 3.10.0
package trailhead
import scala.reflect.Selectable.reflectiveSelectable
class Hut(val name: String, val beds: Int):
def describe(sleeps: Int): String = s"$name sleeps $sleeps of $beds"
type Named = { def name: String }
type Shelter = { val beds: Int; def describe(sleeps: Int): String }
def label(x: Named): String = x.name.toUpperCase
@main def run(): Unit =
val h = Hut("Alpine Hut", 20)
println(label(h))
val s: Shelter = h
println(s.beds)
println(s.describe(12))
println(label(new { def name = "Anonymous" }))
ALPINE HUT
20
Alpine Hut sleeps 12 of 20
ANONYMOUS
Source: Scala 3 Book: Structural types
Hut does not extend anything: it only has the right members. A val member in the refinement accepts a val or a def
without parameters in the class, and a def accepts a method of the same name and signature. The program printed the same
output on 3.3.8 and 3.9.0.
Without the import
Without reflectiveSelectable in scope the member access is an error that names the fix.
//> using scala 3.10.0
package trailhead
class Hut(val name: String)
type Named = { def name: String }
def label(x: Named): String = x.name
@main def run(): Unit = println(label(Hut("A")))
[error] ./s2.scala:6:31
[error] Found: (x : trailhead.Named)
[error] Required: Selectable | Dynamic
[error]
[error] The following import might fix the problem:
[error]
[error] import scala.reflect.Selectable.reflectiveSelectable
[error]
[error] def label(x: Named): String = x.name
[error] ^
In Scala 2 the same code compiled with a feature warning unless import scala.language.reflectiveCalls was in scope. In
Scala 3 the rule is stricter and the import is a conversion rather than a language-feature flag. Importing
reflectiveSelectable at the use site makes the cost visible in the code that pays it.
The reflection cost
reflectiveSelectable looks the member up with java.lang.reflect on every call (Java reflection, the reference says so
explicitly), which is much slower than a virtual call and fails at run time if the member does not exist or is not
accessible. The example calls a structural method 100000 times, and also hands a class with a private name to a method
that wants a public one.
//> using scala 3.10.0
package trailhead
import scala.reflect.Selectable.reflectiveSelectable
class Hut(val name: String)
type Named = { def name: String }
def label(x: Named): String = x.name
class Priv private (private val name: String)
@main def run(): Unit =
println(label(Hut("A")))
val n = 100000
val h = Hut("B")
var i = 0; var len = 0
while i < n do { len += label(h).length; i += 1 }
println(len)
try println(label(Priv.apply())) catch case e: Exception => println(e.getClass.getName)
object Priv:
def apply() = new Priv("x")
A
100000
java.lang.NoSuchMethodException
The last line is a run-time failure the type checker did not prevent: label(Priv()) compiled, but the reflective lookup
found no public name method and threw NoSuchMethodException. Prefer a trait, a type class or a function parameter whenever you control the
types; keep structural types for adapters over classes you cannot change, small scripts and tests.
Writing your own Selectable
scala.Selectable is a marker trait with no required members. If a value’s type is a subtype of Selectable, the compiler
rewrites a member access that it cannot resolve statically:
-
x.name, for a refinementval name: Tor a parameterlessdef name: T, becomesx.selectDynamic("name").asInstanceOf[T]. -
x.add(2, 3), for a refinement method, becomesx.applyDynamic("add")(2, 3).asInstanceOf[R]. If the class declaresapplyDynamic(name: String, paramClasses: Class[?]*)(args: Any*), the parameter classes of the refinement method are passed too, so an implementation can pick an overload.
The class chooses the storage: a Map, a JSON tree, a database row, a Java object. A subtype of the class with a refinement
gives the compiler something to check against. The first example is a record of fields.
//> using scala 3.10.0
package trailhead
class Record(elems: (String, Any)*) extends Selectable:
private val fields = elems.toMap
def selectDynamic(name: String): Any = fields(name)
type Waypoint = Record { val name: String; val elevation: Int }
@main def run(): Unit =
val w = Record("name" -> "Col", "elevation" -> 2100).asInstanceOf[Waypoint]
println(w.name)
println(w.elevation + 1)
Col
2101
The cast is how a loosely typed value gets the type of its expected shape: the compiler trusts the refinement and the
selectDynamic implementation has to deliver it. Accessing a name the refinement does not declare is a compile error, which
is the advantage over a Map[String, Any]: with the extra line w.nothing the compiler reports
value nothing is not a member of trailhead.Waypoint - did you mean w.notify? or perhaps w.toString?. The refinement is
type-checked, the data is not: if the map holds no "elevation" key the failure happens at run time, in selectDynamic.
The program compiled and ran on 3.3.8 and on 3.9.0.
Method calls go through applyDynamic. The example dispatches add and greet to functions stored in a table.
//> using scala 3.10.0
package trailhead
class MethodCalls(elems: (String, Seq[Any] => Any)*) extends Selectable:
private val table = elems.toMap
def applyDynamic(name: String, ctags: Class[?]*)(args: Any*): Any = table(name)(args)
type Calc = MethodCalls { def add(a: Int, b: Int): Int; def greet(who: String): String }
@main def run(): Unit =
val c = MethodCalls(
"add" -> (args => args(0).asInstanceOf[Int] + args(1).asInstanceOf[Int]),
"greet" -> (args => s"Hello ${args(0)}")
).asInstanceOf[Calc]
println(c.add(2, 3))
println(c.greet("trail"))
5
Hello trail
Mutable fields in refinements (Scala 3.5)
A refinement may declare a var, but only since Scala 3.5. Assignments x.units = "mi" are not routed to
selectDynamic. They become a call of the setter method units_=, which for a Selectable goes through applyDynamic
with the name units_$eq (the JVM encoding of =). The example stores the fields in a mutable map.
//> using scala 3.10.0
package trailhead
import scala.collection.mutable
class MutableRecord(init: (String, Any)*) extends Selectable:
private val fields = mutable.Map(init*)
def selectDynamic(name: String): Any = fields(name)
def applyDynamic(name: String)(args: Any*): Any =
println(s"applyDynamic($name)")
if name.endsWith("_$eq") then fields(name.stripSuffix("_$eq")) = args.head
else fields(name)
type Settings = MutableRecord { var units: String; var zoom: Int }
@main def run(): Unit =
val s = MutableRecord("units" -> "km", "zoom" -> 10).asInstanceOf[Settings]
println(s.units)
s.units = "mi"
s.zoom = s.zoom + 2
println(s"${s.units} ${s.zoom}")
km
applyDynamic(units_$eq)
applyDynamic(zoom_$eq)
mi 12
Reads still go to selectDynamic (the first km line has no applyDynamic trace). Writing s.zoom += 2 did not compile on
3.10.0 (value += is not a member of Int), so spell the update out. On 3.3.8 and 3.4.3 the refinement is rejected with
refinement cannot be a mutable var. You can use an explicit getter units and setter units_= instead; on 3.5.2 and 3.9.0 the
program above printed the same output as on 3.10.0.
Typed fields with named tuples (Scala 3.7)
Instead of casting to a refinement, a Selectable can declare a type member Fields: a named-tuple type that lists its
fields and their types. Then w.name and w.elevation are statically typed from Fields with no refinement. This needs
named tuples, which are stable in 3.7 and later.
//> using scala 3.10.0
package trailhead
class Row[F <: scala.NamedTuple.AnyNamedTuple](data: Map[String, Any]) extends Selectable:
type Fields = F
def selectDynamic(name: String): Any = data(name)
type WaypointRow = Row[(name: String, elevation: Int)]
@main def run(): Unit =
val w = Row[(name: String, elevation: Int)](Map("name" -> "Col", "elevation" -> 2100))
val n: String = w.name
val e: Int = w.elevation
println(s"$n $e")
println(w.elevation + 1)
Col 2100
2101
This ran on 3.7.4, 3.9.0 and 3.10.0. On 3.6.4 the named-tuple type is an experimental feature and is rejected, and on 3.3.8 the syntax does not parse. This is the form to use for rows or JSON objects whose fields are known from a schema. Source: Scala 3 Reference: Named Tuples
scala.Dynamic
A class that extends scala.Dynamic can be called with any member name. Unlike Selectable, there is no refinement that
declares the names. The compiler rewrites an access it cannot resolve:
| Source | Rewritten to |
|---|---|
|
|
|
|
|
|
|
|
The call is rewritten only when no real member of that name exists. A class that extends Dynamic needs
import scala.language.dynamics (or the option -language:dynamics), otherwise the compiler reports extension of type
scala.Dynamic needs to be enabled. The example answers all four forms.
//> using scala 3.10.0
package trailhead
import scala.language.dynamics
import scala.collection.mutable
class Bag extends Dynamic:
private val m = mutable.Map.empty[String, Any]
def selectDynamic(name: String): Any = m.getOrElse(name, s"<no $name>")
def updateDynamic(name: String)(value: Any): Unit = m(name) = value
def applyDynamic(name: String)(args: Any*): String = s"$name(${args.mkString(", ")})"
def applyDynamicNamed(name: String)(args: (String, Any)*): String = s"$name named ${args.mkString(", ")}"
@main def run(): Unit =
val b = Bag()
b.title = "Ridge"
println(b.title)
println(b.missing)
println(b.go(1, 2))
println(b.route(from = "A", to = "B"))
println(b.title.getClass.getSimpleName)
Ridge
<no missing>
go(1, 2)
route named (from,A), (to,B)
String
Source: Scala API: scala.Dynamic
All accesses have the static result type that the method declares, so b.title is an Any and b.title.length does not
compile (value length is not a member of Any). Dynamic is therefore good for scripting-style DSLs, wrappers over untyped
data such as a JSON document, and bridges to dynamic languages, and poor for anything where the compiler should check names.
Typos in member names are run-time results rather than compile errors, which is the trade-off to weigh against a typed
Selectable.
Dynamic or Selectable
Selectable and Dynamic are related (the Scala 3 Reference has a section on their relation, cited below): Selectable
serves names that are declared in a refinement and Dynamic serves names that are not. Choose by who knows the names.
| Question | Selectable with a refinement |
Dynamic |
|---|---|---|
Where are the names declared? |
In a refinement or |
Nowhere; any name compiles |
What is the static type of an access? |
The type in the refinement |
|
What does a typo cost? |
A compile error |
A run-time lookup of a name that does not exist |
Language import |
|
|
Typical use |
Records, rows, adapters over classes you cannot change |
Scripting DSLs and untyped wrappers |
Scala 2, Java, Kotlin and other languages
-
Scala 2. Structural calls worked out of the box with reflection after
import scala.language.reflectiveCalls, and a feature warning otherwise. In Scala 3 the call needs thereflectiveSelectableconversion or aSelectablereceiver. TheDynamictrait existed in Scala 2.10 and later with the same rewrite rules and the same language import. -
Java. Types are nominal. Duck typing needs an interface or reflection by hand (
getMethod("name").invoke(x)), which is whatreflectiveSelectabledoes for you. See Lambdas & Method References for the usual Java answer, a functional interface. -
Kotlin. Types are nominal too; the closest tool is an interface plus delegation, or
Mapdelegated properties. Kotlin has no structural type, but see Type-Safe Builders & DSLs for dynamic-looking DSLs. -
TypeScript. Object types are structural by default, as they are in a structural Scala type, and checked entirely at compile time because the objects are plain JavaScript values. See TypeScript.
-
Python. Duck typing is the default and
typing.Protocoldescribes the shape for a type checker. See Python.
Pitfalls
-
Using
reflectiveSelectablein a hot loop. Every call is a reflective lookup (see the 100000-call example). -
Forgetting that the refinement is trusted. A
Recordcast toWaypointthrows when the data lacks a field, and a structural type over a class with a private or differently named member fails withNoSuchMethodException. -
x.f += 1and similar compound assignment on aSelectablevardoes not compile; writex.f = x.f + 1. -
Extending
Dynamicwithout the language import, and treatingDynamicas a way to avoid writing types. Anything that returnsAnypushes the casts onto the caller. -
A structural type in a public API leaks implementation to every client; prefer a trait.
Where the books are outdated
-
Scala 2 material says a structural call is allowed after
import scala.language.reflectiveCalls; Scala 3 usesreflectiveSelectableor aSelectableclass. -
Material before 3.5 says a refinement cannot declare a
var; it can since 3.5. -
Material before 3.7 does not show the
Fieldstype member with named tuples. -
The Scala 3 Book describes
Selectablebut does not list the 3.5 and 3.7 additions above.
Scala 3.9 LTS versus 3.10
-
The
Selectablerewrite,reflectiveSelectableandDynamicbehave the same on 3.3, 3.9 and 3.10 for the programs shown, except that the 3.3 LTS line rejectsvarrefinements (needs 3.5) and named-tupleFields(needs 3.7). -
Both 3.9 and 3.10 compile the
varrefinement and theFieldsexamples with identical output.