Type Classes
Prerequisites: Givens and using clauses, Extension methods
A type class defines behavior that types can opt into, without
inheritance. It's how Scala adds capabilities to types you don't control
— like making Int, String, and your own case classes all
"JSON-encodable".
// 1. The type class: a trait with a type parameter
trait JsonEncoder[A]:
def encode(value: A): String
// 2. Instances for specific types
object JsonEncoder:
given JsonEncoder[Int] with
def encode(value: Int): String = value.toString
given JsonEncoder[String] with
def encode(value: String): String = "\"" + value.replace("\"", "\\\"") + "\""
given JsonEncoder[Boolean] = b => b.toString
// An instance built from another instance
given [A](using enc: JsonEncoder[A]): JsonEncoder[List[A]] =
list => list.map(enc.encode).mkString("[", ",", "]")
// 3. A nice syntax via an extension method
extension [A](value: A)(using enc: JsonEncoder[A])
def toJson: String = enc.encode(value)
@main def typeClasses(): Unit =
println(42.toJson)
println("say \"hi\"".toJson)
println(List(true, false).toJson)
println(List(List(1, 2), List(3)).toJson)
42
"say \"hi\""
[true,false]
[[1,2],[3]]
- The instances live in the companion object, so they're found automatically — no import needed.
- A single-method trait can be implemented with a lambda
(
b => b.toString). - The
Listinstance works for anyAthat has an encoder — the compiler assemblesJsonEncoder[List[List[Int]]]from smaller pieces. - Asking for a type with no instance is a compile-time error, not a runtime surprise.
Your own types
trait Show[A]:
extension (a: A) def show: String
case class Money(amount: BigDecimal, currency: String)
object Money:
given Show[Money] with
extension (m: Money) def show: String = s"${m.currency} ${m.amount.setScale(2)}"
def printAll[A: Show](items: List[A]): Unit =
items.foreach(item => println(item.show))
@main def showing(): Unit =
printAll(List(Money(BigDecimal(5), "EUR"), Money(BigDecimal("19.9"), "USD")))
EUR 5.00
USD 19.90
[A: Show] is a context bound — shorthand for "there must be a
given Show[A]".
Deriving instances
Writing instances for every case class is tedious. Scala 3 can derive them:
// illustrative
case class User(name: String, age: Int) derives upickle.default.ReadWriter
// or with circe:
case class Order(id: Int, total: BigDecimal) derives io.circe.Codec
Libraries implement derives with Scala 3's Mirror type, which describes
a case class's fields at compile time. Type classes power JSON libraries,
Ordering, Numeric, and the whole of Cats and ZIO.