Scala - Structural Types


Scala is primarily a nominally typed language. Types are equal only if these have the same name. But, sometimes, types with different names share common behaviors. And so, modifying the type hierarchy to create relationships between these types may not be possible. In such cases, Scala structural types provide solutions for writing polymorphic code that is checked at runtime for type safety with flexibility.

Using Structural Types for Type-Safe Database Access

One significant use case for structural types is modeling database access. In dynamically typed languages, it is easy to represent a row as a record (or object) and select entries using simple dot notation (e.g., row.columnName). But, achieving the same in statically typed languages like Scala requires defining a class for every possible row configuration and setting up a mapping scheme. But it leads to a lot of boilerplate code. It drives developers to use simpler but less type-safe schemes, like passing column names as strings (e.g., row.select("columnName")).

Structural types use dot notation in dynamic contexts without sacrificing the advantages of static typing. So, developers can use dot notation and configure how fields and methods should be resolved at runtime.

Structural Types in Action

Consider a scenario where you want to define a type Flyer for any object that has a fly method -

type Flyer = { def fly(): Unit }
def callFly(thing: Flyer): Unit = thing.fly()

def callFly2(thing: { def fly(): Unit }): Unit = thing.fly()

def callFly3[T <: { def fly(): Unit }](thing: T): Unit = thing.fly()

In this example, we can declare a structural type in various contexts, like type aliases, method parameter types, and type bounds. The Scala compiler ensures that the required methods exist. So, it provides compile-time type safety while using reflection under the hood to call the methods.

Preventing Resource Leaks

A practical example of structural types is preventing resource leaks by ensuring resources are always closed. Instead of depending on try-catch blocks and conventions. You can use structural types to create a flexible control structure -

type Closable = { def close(): Unit }

def using(resource: Closable)(fn: () => Unit): Unit = {
  try {
    fn()
  } finally {
    resource.close()
  }
}

using(new java.io.FileInputStream("file.txt")) {
  () => 
    // Code using the file
}

The using function closes resources after use. Structural types call the close method. The reflection-based call to close is efficient. Rest of the code inside the function remains statically typed.

Extending Structural Types

Scala structural types are implemented using the Selectable trait. So dynamic member selection through the selectDynamic method -

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

The Selectable trait uses dynamic access to fields and methods. For instance, a record class can use Selectable to map field names to their values dynamically -

case 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]
println(s"${person.name} is ${person.age} years old.") // Emma is 42 years old.

Local and Anonymous Classes

Local and anonymous classes extending Selectable can have more refined types. So, its structural dispatch -

trait Vehicle extends reflect.Selectable {
  val wheels: Int
}

val i3 = new Vehicle {
  val wheels = 4
  val range = 240
}

println(i3.range) // Works because Vehicle extends Selectable

If Vehicle does not extend Selectable. Its accessing range would result in a compilation error.

Comparison with scala.Dynamic

While both structural types and scala.Dynamic give dynamic member selection. Structural types provide type safety with correspondence between structural type and underlying value. scala.Dynamic is used for more flexible reflective access operations but without the same level of type safety. Structural types, with the selectDynamic and applyDynamic methods. These provide more structured approach to dynamic member access.