Back

Type Classes, Givens and Extensions in Scala 3

/ 4 min read

Type Classes, Givens and Extensions in Scala 3

Last Updated:

Recently I have returned to the exploration of Functional Programming in Scala 3. I decided to explore the structure and hierarchy of the Cats library and how the various elements of Category Theory come together.

Directly from the Cats website, ‘Cats is a library which provides abstractions for functional programming in the Scala programming language’.

The fundamental building block of these abstractions is the Type Class.

Type Classes

We use Type Classes to define a ‘kind’ of behaviour. Let’s look at an example:

trait JSONSerializer[T] {
def serialize(value: T): String
}

This type class declares the abstract behavour serialize. Let’s expand on exactly what that means. It means that if this type class is implemented for some type T, then type T would be able to behave in the JSONSerializer way.

This implies that in order to make use of this class type, we need to implement the class type for some type T. Let’s do that for Int.

val intJSONSerializer = new JSONSerializer[Int] {
def serialize(value: Int): String = s"$value"
}

Now that we have an instance of JSONSerializer[T] for Int, we are able to use this instance to call the type class’ methods.

val mySerializedInt = intJSONSerializer.serialize(47)
// 47

It would be reasonable to define a more general API as follows:

def serializeSomeValue[T](value: T)(serializer: JSONSerializer[T]): String =
serializer.serialize(value)
val mySecondSerializedInt = serializeSomeValue(58)(intJSONSerializer)
// 58
/*
Note the currying of parameters. Both these definitions are equivalent.
def myFunction(a: Int)(b: Int): Int = ???
def myFunction(a: Int, b: Int): Int = ???
*/

The inconvienience now is that we need to pass the correct JSONSerializer for the correct type T of value. Enter ‘givens’.

Givens

Scala 3 allows us to declare a value as ‘given’, meaning it can be automatically substituted by the compiler into any parameter which is declared as ‘using’ and which has the same type. Let’s look at a simple example:

def add(a: Int)(using b: Int) = a + b
given Int = 5
val result = add(4)
// 9

First we declare the value 5 as a given Int. This means that for any using parameter for which this given is in scope and which has a matching type of Int, the value 5 will be substituted.

When we call our add function, we do not need to specify the second parameter, since our ‘in scope Int’ of 5 will be substituted.

Using this same technique, we can simplify our previous JSONSerializer example. We can declare our intJSONSerializer as a given JSONSerializer[Int] and then simply let the compiler substitute the function parameter.

def serializeSomeValueWithGiven[T](value: T)(using serializer: JSONSerializer[T]): String =
serializer.serialize(value)
given JSONSerializer[Int] = intJSONSerializer
val myThirdSerializedInt = serializeSomeValueWithGiven(23)
// 23

This is already better, but since we are operating on a single unary parameter, it would be more convenient if we could call the function as a method on the type T instead.

Extensions

In order to add methods to existing types, we define an ‘extension’.

Looking at the example below, let’s explain the syntax in English.

Define an extension for any value value of type T if a given value (let’s call it serializer) of type JSONSerializer[T] exists in scope.

If the above extension clause is true, the compiler will make available the methods defined within the extension body to all instances of type T.

extension [T](value: T)(using serializer: JSONSerializer[T]) {
def serializeTheValue(): String = serializer.serialize(value)
}
val myFourthSerializedInt = 45.serializeTheValue()
// 45

Complete Example

Let’s now look at the full implementation of our example, with the intermediate explanatory values removed.

// -- Example Data Class
case class Person(name: String, age: Int)
// -- Type Class
trait JSONSerializer[T] {
def serialize(value: T): String
}
// -- Syntax Extensions
extension [T](value: T)(using serializer: JSONSerializer[T]) {
def serialize(): String = serializer.serialize(value)
}
// -- Given Type Specific Implementations
given JSONSerializer[Int] = new JSONSerializer[Int] {
def serialize(value: Int): String = s"$value"
}
given JSONSerializer[String] = new JSONSerializer[String] {
def serialize(value: String): String = s"\"$value\""
}
given JSONSerializer[Person] = new JSONSerializer[Person] {
def serialize(value: Person): String =
s"{\"name\":${value.name.serialize()},\"age\":${value.age.serialize()}}"
}
// -- Usage
val jsonInt = 56.serialize()
// 56
val jsonString = "Hello, world!".serialize()
// "Hello, world!"
val jsonPerson = Person("Jenny", 31).serialize()
// "{"name":"Jenny","age":31}"

Outro

This was an initial look at Type Classes, Givens and Extensions in Scala 3.

In the next chapter I will take a look at the type classes from Category Theory that make up most functional libraries and specifically focus on the Cats library.