Type Classes

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala 3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). Type classes need nothing newer than Scala 3.0 in their classic form. The compact given name: [A: Show] => ... syntax used for conditional instances arrived in Scala 3.6, so it is available on the 3.9 LTS line but not on the 3.3 LTS line; the older given ... with form works everywhere and is shown next to it where it matters. Examples were run on 3.10.0 only, so statements about 3.9 are taken from the release notes, not from a run.

A type class is a way to add behavior to a type without changing the type and without inheritance. You describe the behavior as a generic trait, Show[A], you provide an instance of it for every type that supports it, and generic code asks the compiler to find the instance that matches. In Scala 3 each ingredient is a plain language feature: the trait declares the operations, given instances supply them, using clauses and context bounds request them, and extension methods make the call look like a method on the value. This page builds the pattern step by step. The individual features are covered in Givens & Using Clauses, Context Bounds & Deferred Givens and Extension Methods; the next page, Type Class Derivation, shows how to get instances for free. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails.

The pattern at a glance

A type class has three parts, and the compiler wires them together at each use site.

flowchart LR TC["Type class trait
Show[A]"] I1["given Show[Int]"] I2["given Show[Trail]"] I3["given Show[List[A]]
needs Show[A]"] U1["Use site
def report[A: Show]"] U2["Use site
trail.show"] I1 -- instance of --> TC I2 -- instance of --> TC I3 -- instance of --> TC U1 -- "using Show[A]" --> TC U2 -- "extension via Show[Trail]" --> TC I3 -. "needs" .-> I1 I3 -. "needs" .-> I2

The trait on the left defines the contract. The instances in the middle can live anywhere the compiler can find them, including in code that does not own the type. The use sites on the right never name an instance: they state the requirement (a using parameter or a context bound) and the compiler picks the instance at compile time.

A first type class: Show

Show[A] turns a value of type A into a human-readable string. The trait declares one operation as an extension method, so that once an instance is in scope you call value.show. The companion object holds the instances: the compiler searches the companion of the type class when it looks for an instance, so instances placed there are found without any import in the implicit scope of the type class. The call syntax, however, needs the extension method itself to be visible, which is why the program below imports the givens.

//> using scala 3.10.0

package trailhead

case class Trail(name: String, lengthKm: Double, ascentM: Int)
case class Hut(name: String, beds: Int)

trait Show[A]:
  extension (a: A) def show: String

object Show:
  def apply[A](using s: Show[A]): Show[A] = s

  given Show[Int] with
    extension (a: Int) def show: String = a.toString

  given Show[String] = new Show[String]:
    extension (a: String) def show: String = s"\"$a\""

  given Show[Trail]:
    extension (t: Trail) def show: String = s"${t.name} (${t.lengthKm} km, +${t.ascentM} m)"

  given listShow: [A: Show] => Show[List[A]]:
    extension (xs: List[A]) def show: String = xs.map(_.show).mkString("[", ", ", "]")

def report[A: Show](items: List[A]): String =
  items.map(_.show).mkString("\n")

import Show.given

@main def run(): Unit =
  println(42.show)
  println("Pico".show)
  println(Trail("Ridge", 8.5, 900).show)
  println(List(1, 2, 3).show)
  println(List(Trail("Ridge", 8.5, 900), Trail("Lake", 3.0, 120)).show)
  println(report(List(Trail("Ridge", 8.5, 900))))
  println(Show[Int].show(7))
42
"Pico"
Ridge (8.5 km, +900 m)
[1, 2, 3]
[Ridge (8.5 km, +900 m), Lake (3.0 km, +120 m)]
Ridge (8.5 km, +900 m)
7

The example uses four different given forms on purpose.

  • given Show[Int] with is the legacy form, written with with. It works on every Scala 3 version and is the only form that accepts a bare type and a body on 3.0 to 3.5.

  • given Show[String] = new Show[String]: ... is an alias given, a given defined by an ordinary expression.

  • given Show[Trail]: is the form recommended since 3.6: a colon introduces the body, and with is no longer needed.

  • given listShow: [A: Show] => Show[List[A]]: is a conditional instance. It exists only when Show[A] exists, so it produces Show[List[Int]] and Show[List[Trail]] but nothing for List[Hut]. The name listShow is optional in this syntax; it is written out here so the instance can be referred to in an import or an error message.

Show.apply is the conventional summoner: Show[Int] returns the instance in scope, which is shorter than summon[Show[Int]] and gives the call site an intention-revealing name. Both Show[Int].show(7) (calling the extension as an ordinary method with the receiver as argument) and 7.show work.

Requesting an instance

Generic code asks for an instance in one of three ways, and they are interchangeable.

def report1[A](items: List[A])(using s: Show[A]): String = items.map(_.show(using s)).mkString("\n")
def report2[A](items: List[A])(using Show[A]): String = items.map(_.show).mkString("\n")
def report3[A: Show](items: List[A]): String = items.map(_.show).mkString("\n")
  • report1 names the evidence parameter and passes it explicitly, as (using s), when needed.

  • report2 leaves the parameter anonymous; the compiler still resolves .show against it.

  • report3 uses a context bound, A: Show, which is sugar for the anonymous using Show[A] of report2. It reads as "A must have a Show".

The context bound is the idiomatic spelling when the instance is only passed along. On Scala 3.6 and later you can name it, A: Show as s, which gives the instance a name without writing the using clause. The details of both forms are on Context Bounds & Deferred Givens.

Missing instances and ambiguity

Because instances are found at compile time, forgetting one is a compile error, not a runtime failure. Calling .show on a Hut when the instance is not in scope produces:

[error] ./miss.scala:32:11
[error] value show is not a member of trailhead.Hut
[error]   println(Hut("Refugio", 40).show)
[error]           ^^^^^^^^^^^^^^^^^^^^^^^

When a value has no extension at all, the message is as above. When an instance exists but its extension method is not imported, the compiler adds that the method "could be made available as an extension method" and suggests the matching import trailhead.Show.given_Show_Trail. That second case is the one that surprises newcomers: the instances for Show live in the companion of Show, which is in the implicit scope of Show[A] but not of Trail, so trail.show needs import Show.given. Passing the value to a function that has a context bound does not need the import, because the search for Show[Trail] does look into the companion of Show.

A library can improve the message with @implicitNotFound, which replaces the default text for a missing instance of a trait. The type variable is interpolated with ${A}.

//> using scala 3.10.0

package trailhead

import java.time.Duration
import scala.annotation.implicitNotFound

@implicitNotFound("No Describe[${A}] in scope: define a given, or import one with `import Instances.given`")
trait Describe[A]:
  extension (a: A) def describe: String

// A type we do not own, from the JDK
object Instances:
  given Describe[Duration]:
    extension (d: Duration) def describe: String = s"${d.toHours}h ${d.toMinutesPart}m"
  given Describe[Int]:
    extension (n: Int) def describe: String = s"int $n"

import Instances.given

def announce[A: Describe](a: A): String = s"Today: ${a.describe}"

@main def run(): Unit =
  println(announce(Duration.ofMinutes(185)))
  println(announce(3))
Today: 3h 5m
Today: int 3

Asking for announce(Hut("x")) on the same program fails with the custom text:

[error] ./nf.scala:25:29
[error] No Describe[trailhead.Hut] in scope: define a given, or import one with `import Instances.given`
[error]   println(announce(Hut("x")))
[error]                             ^

Two instances for the same type that the compiler cannot rank are ambiguous, and the error names both:

[error] ./amb.scala:13:22
[error] Ambiguous given instances: both object short and object long match type Describe[Int] of parameter x$2 of method announce
[error]   println(announce(3))
[error]                      ^

The rules that decide between candidates, such as nesting level, specificity and the priority given to the companion, are in Conversions, into & Implicit Resolution.

Retroactive instances and the expression problem

The Describe program above gave a Describe instance to java.time.Duration, a class that nobody on this project can edit. That is retroactive polymorphism: the type and the capability are defined independently and joined later by a third party. It is the main reason to prefer a type class over an interface.

This is a concrete answer to the expression problem, the old tension between two directions of extension.

  • With an object-oriented hierarchy it is easy to add a new type (a new subclass) but hard to add a new operation (every class must change, which is impossible for code you do not own).

  • With a pattern match over a closed family it is easy to add a new operation (one new function) but hard to add a new type (every function must change).

  • With a type class, a new operation is a new trait, and a new type is a new instance. Neither requires editing existing code, and the compiler checks that every call site has an instance.

The price is that instance selection is static: the instance is chosen from the static type of the argument, not from the runtime class. A List[Segment] of mixed subclasses uses Show[Segment], never a per-subclass instance, unless Show[Segment] itself dispatches. This is also why type classes cannot replace virtual dispatch when the concrete type is unknown until runtime.

Ordering: a type class in the standard library

You already use a type class whenever you sort. Ordering[A] is the standard-library type class for total orders, and sorted, max, min, sortBy and SortedSet take it as a using parameter. A given Ordering[Trail] therefore changes how every one of those methods orders trails, and an explicit (using ...) picks a different one for a single call.

//> using scala 3.10.0

package trailhead

case class Trail(name: String, lengthKm: Double, ascentM: Int)

given trailByLength: Ordering[Trail] = Ordering.by(_.lengthKm)

def sortedNames(trails: List[Trail])(using Ordering[Trail]): List[String] =
  trails.sorted.map(_.name)

@main def run(): Unit =
  val trails = List(Trail("Ridge", 8.5, 900), Trail("Lake", 3.0, 120), Trail("Pass", 12.0, 1400))
  println(sortedNames(trails))
  println(sortedNames(trails)(using Ordering.by(-_.ascentM)))
List(Lake, Ridge, Pass)
List(Pass, Ridge, Lake)

Java’s Comparator<T> plays the same role, but it is always passed by hand: list.sort(comparator). Scala keeps the instance as a value, Ordering, and lets the compiler pass it. Ordering also has instances for tuples and options built from the instances of their components, using the same conditional-given mechanism as listShow. The standard-library companions Numeric, Integral, Fractional and Equiv are further examples, and in Scala 3 the CanEqual class used for strict equality is a type class too; see Multiversal Equality.

Semigroup and Monoid

A semigroup is a type with an associative way to combine two values. A monoid is a semigroup with an identity value, the unit. These classes are not in the standard library, but the Scala 3 Reference uses them as the standard example because they are small and carry laws.

//> using scala 3.10.0

package trailhead

case class Trail(name: String, lengthKm: Double, ascentM: Int)

trait Semigroup[A]:
  extension (x: A) def combine(y: A): A

trait Monoid[A] extends Semigroup[A]:
  def unit: A

object Monoid:
  def apply[A](using m: Monoid[A]): Monoid[A] = m

given Monoid[Int]:
  def unit: Int = 0
  extension (x: Int) def combine(y: Int): Int = x + y

given Monoid[String]:
  def unit: String = ""
  extension (x: String) def combine(y: String): String = x + y

given [A] => Monoid[List[A]]:
  def unit: List[A] = Nil
  extension (x: List[A]) def combine(y: List[A]): List[A] = x ++ y

case class Totals(km: Double, ascent: Int)
given Monoid[Totals]:
  def unit: Totals = Totals(0.0, 0)
  extension (x: Totals) def combine(y: Totals): Totals = Totals(x.km + y.km, x.ascent + y.ascent)

def combineAll[A: Monoid as m](xs: List[A]): A =
  xs.foldLeft(m.unit)(_.combine(_))

@main def run(): Unit =
  val trails = List(Trail("Ridge", 8.5, 900), Trail("Lake", 3.0, 120), Trail("Pass", 12.0, 1400))
  println(combineAll(List(1, 2, 3, 4)))
  println(combineAll(List("tr", "ail", "head")))
  println(combineAll(List(List(1), List(2, 3))))
  println(combineAll(trails.map(t => Totals(t.lengthKm, t.ascentM))))
  println(combineAll(List.empty[Int]))
  // the laws, checked on sample values
  val a = Totals(1.0, 10); val b = Totals(2.0, 20); val c = Totals(3.0, 30)
  val u = summon[Monoid[Totals]].unit
  assert(a.combine(b).combine(c) == a.combine(b.combine(c)))
  assert(u.combine(a) == a && a.combine(u) == a)
  println("laws hold for the samples")
10
trailhead
List(1, 2, 3)
Totals(23.5,2420)
0
laws hold for the samples

combineAll uses a named context bound, A: Monoid as m, to obtain the instance as m and call m.unit for the empty list. The anonymous given [A] => Monoid[List[A]] is allowed because, in the 3.6 syntax, a conditional given with only type parameters can stay anonymous; the compiler synthesizes the name given_Monoid_List.

Laws

A type class is more than a signature. Each class comes with laws, equations that every instance must satisfy, and generic code relies on them. For a monoid:

  • Associativity: (a combine b) combine c == a combine (b combine c).

  • Left and right identity: unit combine a == a and a combine unit == a.

The compiler cannot check these. combineAll is safe to rewrite as a parallel or divide-and-conquer fold only if the instance obeys associativity. A subtraction "instance" for Int would compile and break that guarantee: (10 - 5) - 2 is 3 while 10 - (5 - 2) is 7. Libraries test laws with property-based tests (see Property-Based Testing); the assert calls above are the smallest possible version of the same idea, run on three samples instead of generated inputs.

Functor and Monad

Some type classes abstract over a type constructor rather than a type: Functor[F[_]] is about List, Option and Future, not about List[Int]. These are higher-kinded type classes, and F[_] is the Scala 3 spelling of a type parameter that itself takes a type argument (see Type Lambdas & Higher Kinds).

//> using scala 3.10.0

package trailhead

trait Functor[F[_]]:
  extension [A](x: F[A]) def map[B](f: A => B): F[B]

trait Monad[F[_]] extends Functor[F]:
  def pure[A](x: A): F[A]
  extension [A](x: F[A]) def flatMap[B](f: A => F[B]): F[B]
  extension [A](x: F[A]) def map[B](f: A => B): F[B] = x.flatMap(a => pure(f(a)))

given Monad[List]:
  def pure[A](x: A): List[A] = List(x)
  extension [A](xs: List[A]) def flatMap[B](f: A => List[B]): List[B] = xs.flatMap(f)

given Monad[Option]:
  def pure[A](x: A): Option[A] = Some(x)
  extension [A](x: Option[A]) def flatMap[B](f: A => Option[B]): Option[B] = x.flatMap(f)

// a Reader monad: a function from a configuration
type Config = Map[String, Int]
type Reader[A] = Config => A
given Monad[Reader]:
  def pure[A](x: A): Reader[A] = _ => x
  extension [A](r: Reader[A]) def flatMap[B](f: A => Reader[B]): Reader[B] = cfg => f(r(cfg))(cfg)

def doubled[F[_]: Functor](xs: F[Int]): F[Int] = xs.map(_ * 2)

@main def run(): Unit =
  println(doubled(List(1, 2, 3)))
  println(doubled(Option(21)))
  val limit: Reader[Int] = _.getOrElse("limit", 0)
  val program: Reader[String] = limit.flatMap(l => (cfg: Config) => s"limit=$l, size=${cfg.size}")
  println(program(Map("limit" -> 8, "extra" -> 1)))
  println(summon[Monad[Option]].pure(5))
List(2, 4, 6)
Some(42)
limit=8, size=2
Some(5)

Monad extends Functor and implements map in terms of flatMap and pure, so an instance only needs to supply pure and flatMap. The same doubled function works for a list and for an option because it only asks for Functor[F]. The Reader instance shows that F need not be a collection: a function from configuration to a value is a monad, and flatMap threads the same configuration to both steps.

Three details are worth knowing.

  • extension [A](x: F[A]) def map declares type parameters on the extension itself, not on the trait, because map is generic in A and B while the trait is generic only in F.

  • The laws for Functor are identity (x.map(identity) == x) and composition; for Monad they are left identity, right identity and associativity of flatMap. As for Monoid, nothing enforces them.

  • The standard library does not ship Functor or Monad, because List, Option and Future already have map and flatMap methods and the for comprehension works through them (see For Comprehensions). Libraries such as Cats and Scalaz define the full hierarchy; Functor and Monad here are teaching versions of what they provide.

Choosing between a type class and the alternatives

Approach Strength Limit

Interface or trait implemented by the type

Dynamic dispatch on the runtime class; no instance lookup.

The type must be edited, so third-party and JDK classes are excluded; one implementation per type.

Pattern match on a sealed hierarchy

One function for every case; the compiler checks exhaustivity.

The hierarchy is closed; adding a case touches every function.

Type class

Operations and types extend independently; several instances per type, selected by scope; works for types you do not own.

Selection is by static type; an instance must exist at compile time; laws are conventions.

Use a type class when the capability is about a type as a whole (ordering, printing, combining, encoding), when it must apply to types you do not control, or when it must be chosen by context (for example two different orderings). Use a trait or a sealed hierarchy when the behavior depends on a value’s runtime class.

Comparison with other languages

  • Java has no equivalent. The closest tools are interfaces such as Comparable<T>, which must be implemented by the type itself, and strategy objects such as Comparator<T>, which are passed explicitly at every call. Since the strategy is an ordinary argument, Java code that wants a default must either overload every method or read it from a static field. Records and sealed interfaces (see Java records and sealed classes) cover the closed-hierarchy half of the expression problem.

  • Kotlin has no type classes either. Extension functions add syntax to existing types (see Extension Functions & Scope Functions), and Kotlin’s context parameters pass values implicitly, but resolution does not search companions or build instances from other instances. Libraries such as Arrow fake a type class with an interface value passed by hand.

  • Rust traits and Haskell type classes are the direct relatives. Scala lets you choose the instance by scope and allow several instances per type, whereas Rust and Haskell enforce coherence: one instance per type, checked globally.

Scala 2 form

Scala 2 had no given, using or extension keywords; the pattern was encoded with implicit, which is why many books and blog posts show it.

//> using scala 2.13.18

trait Show[A] { def show(a: A): String }

object Show {
  def apply[A](implicit s: Show[A]): Show[A] = s
  implicit val intShow: Show[Int] = new Show[Int] { def show(a: Int) = a.toString }
  implicit def listShow[A](implicit s: Show[A]): Show[List[A]] =
    new Show[List[A]] { def show(xs: List[A]) = xs.map(s.show).mkString("[", ", ", "]") }
}

object Main extends App {
  implicit class ShowOps[A](a: A) {
    def show(implicit s: Show[A]): String = s.show(a)
  }
  def report[A: Show](items: List[A]): String = items.map(_.show).mkString("\n")
  println(report(List(1, 2)))
  println(List(1, 2).show)
}
1
2
[1, 2]

This version was compiled and run with Scala 2.13.18 through Scala CLI.

The mapping to Scala 3 is direct.

Scala 2 Scala 3

implicit val or implicit def instance

given

(implicit s: Show[A]) parameter list

(using s: Show[A])

implicit class ShowOps[A](a: A) to add show

an extension method, inside the trait or beside it

implicitly[Show[A]]

summon[Show[A]]

Context bound [A: Show]

Context bound [A: Show], with as naming from 3.6

Scala 3 still accepts implicit definitions, so migrated code keeps building, but the keyword is the legacy spelling and new code should use given, using and extension. The conversion rules and the remaining uses of implicit are collected on Conversions, into & Implicit Resolution.

Pitfalls

  • Forgetting the import. Extension methods defined inside a given are found only when the given is in scope, not when it merely satisfies a using clause. import Show.given imports them; import Show.* does not.

  • Orphan instances in too many places. Put instances in the companion of the type class or of the data type, so the compiler finds them without imports. An instance for a type you do not own belongs in a clearly named object that callers import deliberately, as Instances does above.

  • Ambiguity from overlapping instances. Two unranked instances for one type fail to compile. Remove one, or make one more specific.

  • Static dispatch. Show[Segment] is chosen from the static type; an instance for a subclass is not consulted for a value typed as its supertype.

  • Unlawful instances. A compiling instance can break the laws and break generic code silently. Test the laws.

  • Name clashes between derived and handwritten instances. The next page shows how derives generates a given; if you also write one by hand for the same type you get the ambiguity above.

Where the books are outdated

Books written for Scala 2 or for the first Scala 3 releases show the following forms. The current form is the one used in this page.

  • Encoding a type class with implicit values and implicit class wrappers. Prefer given and extension.

  • given Show[Trail] with for every instance. It still compiles; since 3.6 the recommended spelling is a colon, and conditional instances read given [A: Show] => Show[List[A]]: instead of given [A](using Show[A]): Show[List[A]] with.

  • Naming a context-bound evidence with a hidden parameter or with implicitly. Use A: Show as s from 3.6.

  • Defining Functor and Monad by hand in application code. Use a library for a full hierarchy; the version above is for learning.