Working with Parameterized Traits in Scala 3

This is a recipe from the Scala Cookbook (2nd Edition). This recipe is titled, Working with Parameterized Traits in Scala.

Problem

As you become more advanced in working with Scala types, you want to write a trait whose methods can be applied to generic types, or limited to other specific types.

Solution

Depending on your needs you can use type parameters or type members with traits. This example shows what a generic trait type parameter looks like:

trait Stringify[A]:
    def string(a: A): String

This example shows what a type member looks like:

trait Stringify:
    type A
    def string(a: A): String

Type parameter example

Here’s a complete type parameter example:

trait Stringify[A]:
    def string(a: A): String = s"value: ${a.toString}"

@main def typeParameter =
    object StringifyInt extends Stringify[Int]
    println(StringifyInt.string(100))

Type member example

And here’s the same example written using a type member:

trait Stringify:
    type A
    def string(a: A): String

object StringifyInt extends Stringify:
    type A = Int
    def string(i: Int): String = s"value: ${i.toString}"

@main def typeMember =
    println(StringifyInt.string(42))

TIP: The free book, The Type Astronaut’s Guide to Shapeless, by Dave Gurnell, shows an example where a type parameter and type member are used in combination to create something known as a dependent type.

Discussion

With the type parameter approach you can specify multiple types. For example, this is a Scala implementation of the Java Pair interface that’s shown on this Oracle Generic Types page:

trait Pair[A, B]:
    def getKey: A
    def getValue: B

That demonstrates the use of two generic parameters in a small trait example.

An advantage of parameterizing traits using either technique is that you can prevent things from happening that should never happen. For instance, given this trait and class hierarchy:

sealed trait Dog
class LittleDog extends Dog
class BigDog extends Dog

you can define another trait with a type member like this:

trait Barker:
    type D <: Dog   //type member
    def bark(d: D): Unit

Now you can define an object with a bark method for little dogs:

object LittleBarker extends Barker:
    type D = LittleDog
    def bark(d: D) = println("wuf")

and you can define another object with a bark method for big dogs:

object BigBarker extends Barker:
    type D = BigDog
    def bark(d: D) = println("WOOF!")

Now when you create these instances:

val terrier = LittleDog()
val husky = BigDog()

this code will compile:

LittleBarker.bark(terrier)
BigBarker.bark(husky)

and this code won’t compile, as desired:

// won’t work, compiler error
// BigBarker.bark(terrier)

This demonstrates how a Scala type member can declare a base type in the initial trait, and how more specific types can be applied in the traits, classes, and objects that extend that base type.