Most Scala types are nominal: a value has type Closeable because its class says extends Closeable. A structural type instead describes a shape. { def close(): Unit } is satisfied by any object that has a public close method with that signature, whatever its class declares. That sounds like duck typing, and it is, but with one important difference: the compiler still checks every call against the declared shape. Only the dispatch is dynamic.
That combination makes structural types useful in a few narrow places (adapting third-party classes you cannot change, typed views over dynamic data) and a trap almost everywhere else. This page explains what the compiler actually generates in Scala 2 and Scala 3, the rules and limits the Scala 3 reference documents, what a structural call costs, and how to move off them when they turn up in a hot path.
Refinement types: the general mechanism
A structural type is a special case of a refinement type: a parent type followed by a block of extra member declarations. Animal { def speak(): String } means "an Animal that also has a speak method". When you write only the block, the parent is AnyRef, so { def close(): Unit } is shorthand for AnyRef { def close(): Unit }.
Refinements come in two flavours, and only one of them is expensive. A refinement that fixes a type member, such as Container { type Elem = Int }, is pure type-level information. It costs nothing at runtime, because the member already exists in the parent and the refinement only narrows what the compiler knows. The same is true when a refinement narrows a val or def the parent already declares. The expensive case is a refinement that adds a term member the parent does not have. The JVM has no instruction for "call a method named close on whatever this is", so the compiler has to route the call through something else.
trait Container:
type Elem
def head: Elem
// Type-member refinement: static only, ordinary virtual calls.
def first(c: Container { type Elem = Int }): Int = c.head + 1
// Term-member refinement: close is not a member of AnyRef,
// so this call needs a dispatch mechanism (see below).
type Closeable = { def close(): Unit }Type-member refinements are the bread and butter of path-dependent APIs, covered in Scala path-dependent types. The rest of this page is about the term-member case.
Scala 2: reflective calls behind a feature flag
In Scala 2, a call to a refinement member compiles to Java reflection. The compiler emits code that looks up a java.lang.reflect.Method by name and erased parameter types on the receiver's runtime class, caches the lookup at the call site, boxes primitive arguments, invokes the method and casts the result back. Because this is slow and surprising, Scala 2.10 put it behind a language feature: without import scala.language.reflectiveCalls (or the -language:reflectiveCalls compiler flag) every such call produces a feature warning.
import scala.language.reflectiveCalls
def using[R <: { def close(): Unit }, A](r: R)(f: R => A): A =
try f(r) finally r.close() // reflective call in Scala 2
// Accidental structural type: the anonymous class's extra member
// is part of the inferred type, so p.port is a reflective call.
val p = new AnyRef { def port: Int = 8080 }
p.portThe second example is the more common source of reflective calls in old codebases: nobody wrote a structural type, but the compiler inferred one from an anonymous class. Annotating the val with a named trait removes it. Scala 2 also has a typing restriction worth knowing: a parameter type inside a structural refinement may not refer to an abstract type defined outside that refinement, because the erased signature needed for the reflective lookup would not be known.
Scala 3: Selectable and the rewrite rules
Scala 3 replaced hard-wired reflection with a library hook. Any refinement member access on a value whose type is (or converts to) a subtype of scala.Selectable is rewritten by the compiler into calls on that value. The Scala 3 reference states the rules:
- A field access
v.aof typeUbecomesv.selectDynamic("a").asInstanceOf[U]. - A method call
v.a(args)becomesv.applyDynamic("a")(args).asInstanceOf[R]. Multiple argument lists are flattened into one. - If the Selectable defines the second form,
applyDynamic(name: String, ctags: Class[?]*)(args: Any*), the compiler also passesclassOfof each declared parameter type, so an implementation can resolve overloads by erased signature.
If the receiver is not a Selectable, there is no hidden reflection. The call simply does not compile, which is a deliberate change: the reflective behaviour has to be opted into by name.
reflectiveSelectable: opting into reflection
The opt-in is an implicit conversion, scala.reflect.Selectable.reflectiveSelectable, which wraps any value in a reflect.Selectable whose selectDynamic and applyDynamic use Java reflection on the wrapped object's runtime class. The reference calls the import a visible warning about the cost, which is the right way to read it in code review.
import scala.reflect.Selectable.reflectiveSelectable
type Closeable = { def close(): Unit }
def closeQuietly(r: Closeable): Unit =
try r.close() // rewritten: applyDynamic("close", ...)()
catch case scala.util.control.NonFatal(e) => log.warn(s"close failed: $e")
// Works for classes that share a method name but no interface:
class LegacyPool { def close(): Unit = () }
class VendorSocket { def close(): Unit = () }
closeQuietly(new LegacyPool)
closeQuietly(new VendorSocket)The compile-time check is real: passing an object with no close() is a type error. What is not checked is anything the compiler cannot see, such as a close that exists on the static type but is not public on the runtime class, or how an exception thrown by the target surfaces through the reflective invoke. Test the call path against every concrete class you expect, and catch failures at the boundary rather than letting a reflection exception escape from deep inside business logic.
Programmatic structural types: typed records
The more interesting Scala 3 use is implementing Selectable yourself. The reference's example is a record backed by a map, given a structural type so that field access is typed:
class Record(elems: (String, Any)*) extends Selectable:
private val fields = elems.toMap
def selectDynamic(name: String): Any = fields(name)
type Person = Record { val name: String; val age: Int }
val person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person]
val n: String = person.name // fields("name").asInstanceOf[String]
val a: Int = person.age + 1
// person.email // compile error: not a member of PersonThere is no reflection here at all: person.name is a map lookup plus a cast. The type gives you autocompletion and static checking of field names and types in the code that uses the record. The weak point is the asInstanceOf[Person] that creates it. Nothing verifies that the map really has an Int under "age", so a wrong row fails later, at the first use, with a ClassCastException or NoSuchElementException.
Libraries that build records over database rows or JSON close that gap by generating the structural type and a validating constructor at compile time with macros, the technique described in Scala 3 metaprogramming. If you hand-write records, put the cast in exactly one factory that checks every declared field's presence and runtime class, and make the factory return an Either so a bad row is a value, not a later crash. Note also that Selectable has no updateDynamic: structural records are read-only through the structural type, unlike scala.Dynamic.
The documented limitations
The Scala 3 reference lists restrictions that follow from compiling a structural call to a name plus erased argument classes:
| Limitation | What it means in practice |
|---|---|
| Dependent methods cannot be called structurally | A refinement method whose result type mentions its own parameters (def get(k: Key): k.Value) has no erased form to dispatch on |
| Refinements may not introduce overloads | If the parent already has a method of that name, the refinement must properly override it, not add a sibling overload |
| Parameter erasures must match | The refinement's parameter types must erase to the same classes as the implementation's, or the reflective lookup finds nothing |
| No updateDynamic | Assignment through a structural type is not supported for Selectable |
Two more traps come from erasure rather than from the spec. A pattern match case r: { def close(): Unit } => cannot be checked at runtime, because the JVM sees only Object; the compiler warns that it is unchecked, and it matches anything. And generic signatures are erased: if an unchecked cast (such as an unvalidated record factory) lets an object through, a refinement declaring def put(xs: List[Int]): Unit will happily dispatch to its put(List[String]) at runtime. Generics in general are covered in the Scala type system.
What a structural call costs
A reflective structural call does several things a virtual call does not: a method lookup by name and parameter classes (cached after the first call, but the cache is still consulted), boxing of primitive arguments into an Array[Any], a reflective invoke that the JIT optimises far less well than a direct call, unboxing and a cast on the result, and exception handling around the reflective invoke. A custom Selectable costs whatever its selectDynamic does, typically a hash lookup and a cast. Do not trust a number from a blog post, including this one; measure your JVM and your call shape:
import org.openjdk.jmh.annotations.*
import scala.reflect.Selectable.reflectiveSelectable
trait Sized { def size: Int }
class Box(val n: Int) extends Sized { def size: Int = n }
@State(Scope.Thread)
class StructuralBench:
val nominal: Sized = Box(3)
val structural: { def size: Int } = Box(3)
@Benchmark def nominalCall(): Int = nominal.size
@Benchmark def structuralCall(): Int = structural.sizeRun it with sbt-jmh at a realistic call-site shape (one receiver class, then several) before deciding. The usual result is that the difference is irrelevant for a call made once per request and decisive for a call made inside a per-element loop.
Worked example: from duck-typed close to a type class
A service has a helper withResource[R <: { def close(): Unit }] used for a JDBC connection, a vendor client with close() but no interface, and an in-house pool. A profiler shows the helper on a per-row path through a batch importer. The fix keeps the call sites' shape and moves dispatch to compile time with a type class, which is exactly how scala.util.Using already works through its Releasable type class:
import scala.util.Using
import scala.util.Using.Releasable
// AutoCloseable already has a Releasable instance; add the vendor type once.
given Releasable[VendorClient] with
def release(c: VendorClient): Unit = c.close()
given Releasable[LegacyPool] with
def release(p: LegacyPool): Unit = p.shutdown() // a different name is fine
def importRows(client: VendorClient): Int =
Using.resource(client) { c =>
c.rows().foldLeft(0)((n, row) => n + write(row))
}Three things improved. Dispatch is a resolved given instead of reflection. The legacy pool, whose release method is called shutdown, fits without an adapter class, which the structural type could never express. And a type with no instance is a compile error that names the missing Releasable, not a runtime reflection failure. How the compiler finds those instances is explained in Scala implicit resolution.
Choosing a tool
| Need | Use | Why |
|---|---|---|
| Several of your own classes share behaviour | A trait | Nominal, fastest, documents intent |
| Behaviour for classes you cannot change | A type class | Static dispatch, can rename and adapt |
| One-off adaptation, cold path, Scala 3 | reflectiveSelectable | No boilerplate; the import flags the cost |
| Typed view over maps, rows, JSON | Custom Selectable record | Static field checks, no reflection |
| Narrow a type member | Type refinement | Free at runtime |
| Arbitrary member names at runtime | scala.Dynamic | Untyped by design; supports updateDynamic |
The failure modes cluster on the same few mistakes: a reflective call inherited from an inferred anonymous-class type in a hot loop, a record cast that is never validated, a pattern match on a refinement that silently matches everything, and a refinement on generic parameters that erasure makes meaningless. Each is cheap to find with a search for reflectiveCalls, reflectiveSelectable and asInstanceOf near Selectable.
What to do next
- Search your build for
reflectiveCallsandreflectiveSelectable; list every structural call site and whether it is on a per-request or per-element path. - Annotate vals initialised with anonymous classes with a named trait so no structural type is inferred.
- Replace hot-path structural calls with a trait you own or a type class such as
Releasable. - For records, put the
asInstanceOfin one validating factory returningEither. - Remove any pattern match on a refinement type; match on a nominal type instead.
- If you keep a structural call, add a test for each concrete runtime class it must accept.
- Benchmark the remaining call shapes with the JMH harness above before arguing about cost.