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.

Quick Review of Scala 3’s Existential and Refinement Types
Scala has powerful abstraction mechanisms.

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:

Scala 3

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:

Scala 3

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:

Scala 3

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.

⚠️
Scala 2 reused the in many locations, with different meanings. Scala 3 replaced _ with ? in wildcard (existential) types, which is the symbol used in Java.

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:

Scala 3

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.

💡
Note that we used a different name for the function; though improving the code to be more self-documenting is a reason for it, it is not the only one. Overloading 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:

Scala 3

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:

Scala 3

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.

If you find our content helpful, there are several ways you can support us:

  • The easiest way is to share our articles and links page on social media; it is free and helps us greatly.
  • If you want a great experience during the Chinese New Year, I am renting my timeshare in Phuket. A five-night stay in this resort in Phuket costs 11,582 € on Expedia. I am offering it in USD at an over 40% discount compared to that price. I received the Year of the Snake in style.
Anantara Vacation Club Phuket Mai Khao $$1,390/night
Phuket, Thailand / Posting R1239106

ReedWeek Timeshare Rental

  • If your finances permit it, we are happy over any received donation. It helps us offset the site's running costs and an unexpected tax bill. Any amount is greatly appreciated:
  • Finally, some articles have links to relevant goods and services; buying through them will not cost you more. And if you like programming swag, please visit the TuringTacoTales Store on Redbubble. Take a look. Maybe you can find something you like: