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}

Leave a Comment

Your email address will not be published. Required fields are marked *