This doc page is specific to Scala 3, and may cover new concepts not available in Scala 2. Unless otherwise stated, all the code examples in this page assume you are using Scala 3.
Scala 2 has a weaker form of structural types based on Java reflection, achieved with import scala.language.reflectiveCalls
.
Introduction
Some use cases, such as modeling database access, are more awkward in statically typed languages than in dynamically typed languages.
With dynamically typed languages, it’s 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 a 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 column 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 you’d 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
Here’s an example of a structural type Person
:
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
}
The Person
type adds a refinement to its parent type Record
that defines name
and age
fields.
We say the refinement is structural since name
and age
are not defined in the parent type.
But they exist nevertheless as members of class Person
.
For instance, the following program would print "Emma is 42 years old."
:
val person = Record(
"name" -> "Emma",
"age" -> 42
).asInstanceOf[Person]
println(s"${person.name} is ${person.age} years old.")
The parent type Record
in this example is a generic class that can represent arbitrary records in its elems
argument.
This argument is a sequence of pairs of labels of type String
and values of type Any
.
When you create a Person
as a Record
you have to assert with a typecast that the record defines the right fields of the right types.
Record
itself is too weakly typed, so the compiler cannot know this without help from the user.
In practice, the connection between a structural type and its underlying generic representation would most likely be done by a database layer, and therefore would not be a concern of the end user.
Record
extends the marker trait scala.Selectable
and defines a method selectDynamic
, which maps a field name to its value.
Selecting a structural type member is done by calling this method.
The person.name
and person.age
selections are translated by the Scala compiler to:
person.selectDynamic("name").asInstanceOf[String]
person.selectDynamic("age").asInstanceOf[Int]
A second example
To reinforce what you just saw, here’s another structural type named Book
that represents a book that you might read from a database:
type Book = Record {
val title: String
val author: String
val year: Int
val rating: Double
}
As with Person
, this is how you create a Book
instance:
val book = Record(
"title" -> "The Catcher in the Rye",
"author" -> "J. D. Salinger",
"year" -> 1951,
"rating" -> 4.5
).asInstanceOf[Book]
Selectable class
Besides selectDynamic
, a Selectable
class sometimes also defines a method applyDynamic
.
This can then be used to translate function calls of structural members.
So, if a
is an instance of Selectable
, a structural call like a.f(b, c)
translates to:
a.applyDynamic("f")(b, c)