1. Overview
Scala is mainly a nominally typed language, which means the type of two objects will be equal only if it has the same name.
But what about types that have different names but something in common?. What if we can’t modify the type hierarchy to create a relationship between the types?
In this tutorial, we’ll learn how to use Scala’s structural types to write runtime checked polymorphic code for those occasions when we can’t use inheritance or type classes.
2. If It Quacks Like a Duck
In Scala, we use types to ensure correctness, and we do it by encoding code invariants in the types and relationships between types. The compiler enforces those invariants, guaranteeing our programs are free of certain error categories.
We pay the price for these guarantees; the compiler makes some programs a bit harder to write.
For example; if we try to write a program similar to the following Python 3 code that uses duck typing, the Scala compiler will throw an error:
class Duck:
def fly(self):
print("Ducks fly together")
class Eagle:
def fly(self):
print("Eagles fly better than MJ")
class Walrus:
def swim(self):
print("I am faster on the water than on the land")
def flyLikeAnEagle(animal):
animal.fly()
animals = [Duck(), Eagle(), Walrus()]
for animal in animals:
flyLikeAnEagle(animal)
Clearly, we want the compiler to prevent us from calling the method fly on a Walrus. But it rejecting the calls on the Duck and Eagle objects is an inconvenience.
Python does the type checks at runtime, and it will let us call the fly method on any object that has it. Programmers refer to this feature as duck typing because “if it quacks like a duck, it is a duck.”
2. Gettings Our Ducks in a Row
We could use Scala reflection to write similar code, but the resulting code will be hard to maintain.
Fortunately, Scala allows us to write idiomatic code to achieve the same results as duck typing.
Enter structural types:
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()
As we see in the example above, we can declare a structural type at most places where a type constraint is expected: type alias, method’s parameter types, or a lower type bound.
Scala will generate code that used reflection underneath to call the right method, but with the benefit of compile-time type safety; since the compiler will ensure the required methods exist, eliminating the possibility of runtime exceptions:
"Ducks" should "fly together" in {
callFly(new Duck())
callFly2(new Duck())
callFly3(new Duck())
}
"Eagles" should "soar above all" in {
callFly(new Eagle())
callFly2(new Eagle())
callFly3(new Eagle())
}
"Walrus" should "not fly" in {
// The following code won't compile
// callFly(new Walrus())
// callFly2(new Walrus())
// callFly3(new Walrus())
}
3. A More Serious Example
A classic and practical example of structural types in preventing resource leaks by ensuring resources are always closed. We could use a classic try-catch block, but we’ll depend on everybody following the convention.
We can use structural types to write a flexible control structure:
type Closable = { def close(): Unit }
def using(resource: Closable)(fn: () => Unit) {
try {
fn()
} finally { resource.close() }
}
using(file) {
() =>
// Code using the file
}
As we discussed, the call to close will use reflection, but all the code inside the function we pass will be statically linked.
The cost of a single dynamic call is negligible. Still, in other cases, there might be a benefit of using overloading and specializing some versions of the control construct for types we know in advance.
In this particular case, we could create a statically typed version for the Source class:
def using(file: Source)(fn: () => Unit) {
try {
fn()
} finally {
file.close()
}
}
4. Conclusion
In this article, we explored using structural types for those occasions when writing other polymorphic codes fail.
We also learned the resulting program uses reflection, but we proved that we do not lose type safety. The only disadvantage is the small performance degradation associated with runtime reflection.
As always, the full source code of the article is available over on GitHub.