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.
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] withis the legacy form, written withwith. 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, andwithis no longer needed. -
given listShow: [A: Show] => Show[List[A]]:is a conditional instance. It exists only whenShow[A]exists, so it producesShow[List[Int]]andShow[List[Trail]]but nothing forList[Hut]. The namelistShowis 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")
-
report1names the evidence parameter and passes it explicitly, as(using s), when needed. -
report2leaves the parameter anonymous; the compiler still resolves.showagainst it. -
report3uses a context bound,A: Show, which is sugar for the anonymoususing Show[A]ofreport2. 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
Source: Scala API: implicitNotFound
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)
Source: Scala 3 Book: Type classes
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 == aanda 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)
Source: Scala 3 Reference: Functors
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 mapdeclares type parameters on the extension itself, not on the trait, becausemapis generic inAandBwhile the trait is generic only inF. -
The laws for
Functorare identity (x.map(identity) == x) and composition; forMonadthey are left identity, right identity and associativity offlatMap. As forMonoid, nothing enforces them. -
The standard library does not ship
FunctororMonad, becauseList,OptionandFuturealready havemapandflatMapmethods and theforcomprehension works through them (see For Comprehensions). Libraries such as Cats and Scalaz define the full hierarchy;FunctorandMonadhere are teaching versions of what they provide.
Source: Scala 3 Reference: Monads
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 asComparator<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 |
|---|---|
|
|
|
|
|
an |
|
|
Context bound |
Context bound |
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
usingclause.import Show.givenimports 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
Instancesdoes 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
derivesgenerates 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
implicitvalues andimplicit classwrappers. Prefergivenandextension. -
given Show[Trail] withfor every instance. It still compiles; since 3.6 the recommended spelling is a colon, and conditional instances readgiven [A: Show] => Show[List[A]]:instead ofgiven [A](using Show[A]): Show[List[A]] with. -
Naming a context-bound evidence with a hidden parameter or with
implicitly. UseA: Show as sfrom 3.6. -
Defining
FunctorandMonadby hand in application code. Use a library for a full hierarchy; the version above is for learning.