Quick Review of Scala 3’s Existential and Refinement Types
Discover Scala 3's powerful feature: existential and refinement types. Learn how these advanced features can enhance your coding practices, ensuring greater safety and expressiveness in our Scala applications.
Introduction
Scala 3's type system has many advanced features not present in other JVM programming languages, like existential and refinement types.
Both are ways to inform the compiler about expectations that our program with the objects it accepts as inputs or describes the properties of its results without limiting the type to a specific named type.
The difference between them is that the existential type applies any refinements. In contrast, refinement types tighten the constraints on particular types our data and functions take, increasing safety and reliability.
In this tutorial, we will review both and look at simple examples showing how to use them.
Existential Types
To understand existential types, let's start with a simple example:
def count(seq: Seq[Int]): Int =
var result = 0
for (item <- seq) do
result += 1
result
This function counts elements in a sequence, but it is too limiting because of the concrete types used in the signature. We won't be able to use this same function with sequences of Strings or any type different than Int.
An idea would be to use generics:
def count[T](seq: Seq[T]): Int =
var result = 0
for (item <- seq) do
result += 1
result
This is a step forward. It enables us to use the same function with different types. However, it is not perfect; it makes the signature more complex because we need to introduce a type we will not use.
Imagine we could tell the compiler we need to know that a type exists in this position, but we do not care about any detail except it exists:
def count(seq: Seq[?]): Int =
var result = 0
for (item <- seq) do
result += 1
result
Besides making the function generic without the extra complexity, the signature now communicates to users of our function that we are not performing any operations on the elements since the function does not know anything about them.
Bounding the Type
Using the wildcard as a type is on one extreme of the typing scale. With it, we are saying we know there is a type but do not know much else beyond that. At the other extreme is using a concrete class, where we assert we need to know everything or at least quite a lot about it.
But let's imagine a situation in which we need to know some things about the type but do not care about every detail. Suppose we need a function that counts the characters in a sequence of character sequences. For that, we need to limit the input to Character Sequence:
def totalLength(seq: Seq[? <: CharSequence]): Int =
var result = 0
for (item <- seq) do
result += item.length
result
@main
def main() =
println(totalLength(List("Hello", "World")))
This limits the types we can pass into our function to subtypes of CharSequence, allowing us to call the lenght
method on the elements without resorting to reflection.
count
This will result in a compiler error because type bounds are a compile-time construct and are not available at runtime, resulting in the functions with and without bounds having the same signature at runtime.Refinement Types
In the previous section, we wrote a function that could accept sequences with elements of any type that extends CharSequence, but how could we write our function to support any type with the length
property?
In Scala 3, we can use refinement types, we can use them to define structural requirements on types:
import reflect.Selectable.reflectiveSelectable
type WithLenght = { def length: Int }
def totalLength(seq: Seq[WithLenght]): Int =
var result = 0
for (item <- seq) do
result += item.length
result
@main
def main() =
println(totalLength(List("Hello", "World")))
println(totalLength(List(List(1, 2), List(3))))
They are superficially similar to Scala 2's structural types, but in the JVM, they are compiled using invokeDynamic instead of reflection.
Another exciting application of refinement methods is to ensure values adhere to certain invariants, like limiting a list of persons to contain Adults
only:
type True = true
type Adult = Person { def isAdult: True }// require(age > 18)
case class Person(name: String, age: Int):
def isAdult: Boolean = age >= 18
def toAdult: Option[Adult] =
if isAdult then
Some(new Person(name, age) { override val isAdult: True = true })
else
None
val persons = List(
Person("Alain", 42),
Person("Rita", 35),
Person("Lolita", 16)
)
def getAdults(people: Seq[Person]): Seq[Adult] =
for{ p <- people
if p.isAdult } yield p.toAdult.get
class AdultOnlyResort():
def makeReservation(guests: Seq[Adult]) =
for(guest <- guests) do
println(s"Made made reservation for ${guest}" )
@main
def main() =
val resort = AdultOnlyResort()
// resort.makeReservation(persons) // Compilation Error!!
val adults = getAdults(persons)
resort.makeReservation(adults)
Granted, this is a somewhat convoluted example for not much extra type safety, but it still shows the potential in Scala's type system, in a future article we'll discuss a library that takes type refinements to a higher level.
Conclusion
Existential and refinement types are powerful abstraction tools. We've seen how these features enable us to write more flexible, safe, and expressive code.
Existential types enable abstraction over unspecified types, while refinement types enable precise constraints on partially known types.
If you found this exploration into Scala 3's advanced type system enlightening and wish to dive deeper into Scala and other programming languages, consider subscribing.
By subscribing, you can comment and suggest topics for our future articles.
Addendum: A Special Note for Our Readers
I decided to delay the introduction of subscriptions, you can read the full story here.
In the meantime, I decided to accept donations.
If you can afford it, please consider donating:
Every donation helps me offset the running costs of the site and an unexpected tax bill. Any amount is greatly appreciated.
Also, if you are looking to buy some Swag, please visit I invite you to visit the TuringTacoTales Store on Redbubble.
Take a look, maybe you can find something you like: