Scala Type Classes
A type class is a design pattern that adds behavior to types without modifying them and without requiring inheritance. You define an interface (the type class), provide implementations for specific types (instances), and write generic functions that work with any type that has an instance. This is how Scala extends third-party types you cannot change.
The Three Parts of a Type Class
1. Type Class Definition — the trait describing the capability
2. Type Class Instances — given values providing the capability for specific types
3. Generic Functions — functions that use the type class via 'using' parameter
Step 1: Define the Type Class
trait Describable[A]:
def describe(value: A): String
Step 2: Provide Instances
given Describable[Int] with
def describe(n: Int): String = s"number $n"
given Describable[String] with
def describe(s: String): String = s"text '$s'"
given Describable[Boolean] with
def describe(b: Boolean): String = if b then "yes" else "no"
given Describable[List[Int]] with
def describe(l: List[Int]): String = s"list of ${l.length} integers summing to ${l.sum}"
Step 3: Write Generic Functions
def print[A](value: A)(using d: Describable[A]): Unit =
println(d.describe(value))
def printAll[A](values: List[A])(using d: Describable[A]): Unit =
values.foreach(v => println(d.describe(v)))
print(42) // number 42
print("hello") // text 'hello'
print(true) // yes
print(List(1,2,3)) // list of 3 integers summing to 6
printAll(List(1, 2, 3)) // number 1 / number 2 / number 3
printAll(List("a", "b", "c")) // text 'a' / text 'b' / text 'c'
Deriving Instances for Your Own Types
case class Point(x: Double, y: Double)
case class Person(name: String, age: Int)
given Describable[Point] with
def describe(p: Point): String = f"point at (${p.x}%.1f, ${p.y}%.1f)"
given Describable[Person] with
def describe(p: Person): String = s"${p.name}, age ${p.age}"
print(Point(3.0, 4.0)) // point at (3.0, 4.0)
print(Person("Alice", 30)) // Alice, age 30
The Show Type Class (Serialization)
trait Show[A]:
def show(a: A): String
object Show:
def apply[A](using s: Show[A]): Show[A] = s
extension [A](value: A)(using s: Show[A])
def show: String = s.show(value)
given Show[Int] with { def show(n: Int) = n.toString }
given Show[Double] with { def show(d: Double) = f"$d%.2f" }
given Show[String] with { def show(s: String) = s""""$s"""" }
given [A](using sa: Show[A]): Show[List[A]] with
def show(list: List[A]): String =
list.map(sa.show).mkString("[", ", ", "]")
println(42.show) // 42
println(3.14.show) // 3.14
println("hello".show) // "hello"
println(List(1, 2, 3).show) // [1, 2, 3]
println(List("a", "b").show) // ["a", "b"]
The Eq Type Class (Equality)
trait Eq[A]:
def equal(a: A, b: A): Boolean
def notEqual(a: A, b: A): Boolean = !equal(a, b)
given Eq[Int] with { def equal(a: Int, b: Int) = a == b }
given Eq[String] with { def equal(a: String, b: String) = a == b }
case class Point(x: Int, y: Int)
given Eq[Point] with
def equal(a: Point, b: Point): Boolean = a.x == b.x && a.y == b.y
def areEqual[A](a: A, b: A)(using eq: Eq[A]): Boolean = eq.equal(a, b)
println(areEqual(42, 42)) // true
println(areEqual("hello", "world")) // false
println(areEqual(Point(1,2), Point(1,2))) // true
println(areEqual(Point(1,2), Point(3,4))) // false
Type Class vs Inheritance
Inheritance Type Class
──────────────────────────────── ────────────────────────────────────
Must modify the class No modification needed
Tight coupling Loose coupling
Cannot extend third-party types CAN extend third-party types
Single inheritance limit Multiple type class instances
Open for extension requires design Add instances anywhere
Example: Java's Comparable Example: Scala's Ordering, Show
Practical: Serializer Type Class
trait JsonEncoder[A]:
def encode(value: A): String
given JsonEncoder[Int] with { def encode(n: Int) = n.toString }
given JsonEncoder[String] with { def encode(s: String) = s""""$s"""" }
given JsonEncoder[Boolean] with { def encode(b: Boolean) = b.toString }
given [A](using enc: JsonEncoder[A]): JsonEncoder[List[A]] with
def encode(list: List[A]): String =
list.map(enc.encode).mkString("[", ",", "]")
case class Product(name: String, price: Double, inStock: Boolean)
given JsonEncoder[Product] with
def encode(p: Product): String =
s"""{"name":${summon[JsonEncoder[String]].encode(p.name)},"price":${p.price},"inStock":${p.inStock}}"""
def toJson[A](value: A)(using enc: JsonEncoder[A]): String = enc.encode(value)
println(toJson(42)) // 42
println(toJson("hello")) // "hello"
println(toJson(List(1,2,3))) // [1,2,3]
println(toJson(Product("Laptop", 75000.0, true)))
// {"name":"Laptop","price":75000.0,"inStock":true}
