This is an excerpt from the Scala Cookbook, 2nd Edition. This is Recipe 23.7, Creating Meaningful Type Names with Opaque Types.
In keeping with practices like Domain-Driven Design (DDD), you want to give values that have simple types like
Int more meaningful type names to make your code safer and easier to read.
In Scala 3, the solution is to use opaque types to create meaningful type names. For an example of the problem, when a customer orders something on an e-commerce website you may add it to a cart using the
customerId and the
def addToCart(customerId: Int, productId: Int) = ...
Because both types are
Int, it’s possible to confuse them. For instance, developers will call this method with integers like this:
Because both fields are integers, it’s possible to confuse them again later in the code:
// are you sure you have the right id here? if (id == 1000) ...
The solution to this problem is to create custom types as opaque types. A complete solution looks like this:
object DomainObjects: opaque type CustomerId = Int object CustomerId: def apply(i: Int): CustomerId = i given Eql[CustomerId, CustomerId] = Eql.derived opaque type ProductId = Int object ProductId: def apply(i: Int): ProductId = i given Eql[ProductId, ProductId] = Eql.derived
This lets you write code like this:
@main def OpaqueTypes = // import the types import DomainObjects._ // use the `apply` methods val customerId = CustomerId(101) val productId = ProductId(101) // use the types def addToCart(customerId: CustomerId, productId: ProductId) = ... // pass the types to the function addToCart(customerId, productId)
given Eql” portion of the solution also creates a compiler error if you attempt incorrect type comparisons at some future time:
// error: values of types DomainObjects.CustomerId and Int // cannot be compared with == or != if customerId == 1000 // also an error: this code will not compile if customerId == productId ...
When you work in a Domain-driven design (DDD) style, one of the goals is that the names you use for your types should match the names used in the business domain. For example, when it comes to variable types you can say:
A domain expert thinks about things like
Conversely, they don’t think about things like
Beyond DDD, an even more important consideration is functional programming (FP). One of the benefits of writing code in a functional style is that other programmers should be able to look at our function signatures and quickly see what our function does. For example, take this function signature:
def f(s: String): Int
Assuming the function is pure, we see that it takes a
String and returns an
Int. Given only those facts we can quickly deduce that the function probably does one of these things:
Determines the string length
Does something like calculating the checksum of the string
We also know that the function doesn’t attempt to convert the string to an int, because that process can fail, so a pure function that converts a string to an int will return the possible result in an error-handling type, like this:
def f(s: String): Option[Int] def f(s: String): Try[Int] def f(s: String): Either[Throwable, Int]
Given that pure function signatures are so important, we also don’t want to write types like this:
def validate( username: String, email: String, password: String )
Instead, our code will be easier to read and much more type-safe if we create our types like this:
def validate( username: Username, email: EmailAddress, password: Password )
This second approach — using opaque types — improves our code in several ways:
In the first example, all three parameters are strings, so it can be easy to call
validatewith the parameters in the wrong order. Conversely, It will be much more difficult to pass the parameters into the second
validatemethod in the wrong order.
validatetype signature will be much more meaningful to other programmers in their IDEs and in the Scaladoc.
We can add validators to our custom types, so we can validate the
passwordfields when they are created.
Eqlwhen creating opaque types, you can make it impossible for two different types to be compared using
!=. (See the
Eqlrecipe for more details on using
Your code more accurately reflects the verbiage of the domain.
As shown in the Solution, opaque types are a terrific way to create types like
Benefits of the three-step solution
The code in the solution looks like this:
opaque type CustomerId = Int object CustomerId: def apply(i: Int): CustomerId = i given Eql[CustomerId, CustomerId] = Eql.derived
While it’s possible to create an opaque type with this one line of code:
opaque type CustomerId = Int
each step in the three-step solution serves a purpose:
opaque typedeclaration creates a new type named
CustomerId. (Behind the scenes, a
applymethod creates a factory method (constructor) for new
given Eqldeclaration states that a
CustomerIdcan only be compared to another
CustomerId. Attempting to compare a
Intwill create a compiler error; it’s impossible to compare them.
There were several attempts to try to achieve a similar solution in Scala 2:
- Type aliases
- Value classes
- Case classes
Unfortunately all of these approaches had weaknesses, as described in the Opaque Types SIP. The goal of opaque types, as described in that SIP, is that “operations on these wrapper types must not create any extra overhead at runtime while still providing a type safe use at compile time.” Opaque types in Scala 3 have achieved that goal.
|this post is sponsored by my books:|
#1 New Release
FP Best Seller
Learn Scala 3
Learn FP Fast
There are a few rules to know about opaque types:
- They must be defined within the scope of an object, trait, or class.
- The type alias definition is visible only within that scope. (Within that scope, your code can see that a
CustomerIdis really an
- Outside the scope only the defined alias is visible. (Outside that scope, other code can’t tell that
CustomerIdis really an
As an important note for high-performance situations, the SIP also states, “Opaque type aliases are compiled away and have no runtime overhead.”