A type class is a programming pattern that defines behavior for a type without requiring the type to inherit from a particular base class. In Scala 3, the pattern commonly uses a trait, given instances, using clauses, and extension methods.
Define the capability
trait Show[A]:
def show(value: A): String
given Show[Int] with
def show(value: Int): String = value.toString
def render[A](value: A)(using instance: Show[A]): String =
instance.show(value)
The compiler searches for an eligible contextual instance at the call site. This is compile-time resolution, not runtime reflection or automatic proof that an instance is semantically correct.
Add ergonomic syntax
extension [A](value: A)
def shown(using instance: Show[A]): String = instance.show(value)
val text = 42.shown
Extension methods improve call-site syntax but do not modify the original type. Keep names discoverable and avoid imports that make behavior difficult to trace.
Use context bounds and summon
def renderAll[A: Show](values: List[A]): List[String] =
val instance = summon[Show[A]]
values.map(instance.show)
A context bound expresses that a contextual instance is required. Exact desugaring details can evolve; follow documentation for the deployed Scala compiler.
Compose instances carefully
given [A](using showA: Show[A]): Show[List[A]] with
def show(values: List[A]): String =
values.map(showA.show).mkString("[", ", ", "]")
Generic derivation can reduce repetition, but overlapping or surprising instances harm coherence. Put canonical instances in predictable scopes, require explicit imports for alternatives, and avoid broad conversions.
Model a discount rule without hidden state
trait Discount[A]:
def amount(value: A): BigDecimal
final case class Order(total: BigDecimal)
given Discount[Order] with
def amount(order: Order): BigDecimal =
if order.total >= 100 then order.total * BigDecimal("0.10")
else BigDecimal(0)
def finalTotal[A](value: A, total: BigDecimal)(using d: Discount[A]) =
total - d.amount(value)
This illustrates dispatch only. Production money code also needs currency, rounding, validation, rule version, effective dates, stacking policy, audit evidence, and tests.
Test laws and resolution
- Test each instance with representative and boundary values.
- Test algebraic laws when the abstraction claims them.
- Compile examples that prove the intended instance resolves.
- Test that ambiguous imports fail or require explicit selection.
- Avoid global mutable state in contextual instances.
Migrate from Scala 2 deliberately
Scala 2 commonly used implicit val, implicit def, and implicit classes. Scala 3 supports migration paths while recommending given, using, and extension methods. Conversion rules, implicit search, imports, and compiler diagnostics differ; use the official migration guide and pin compiler/library versions.
For larger applications, connect the pattern to Scala in data-intensive systems, typed failure handling in Scala error handling, and interoperability constraints in Scala and Spark.
Originally published November 15, 2018; technically reviewed and substantially updated September 4, 2026.