Givens, using clauses and type classes

Scala · Advanced · 16 min

Type classes power JSON codecs, ordering, numeric abstraction and every major functional library.

Scala 3 contextual abstractions let the compiler pass arguments for you based on their type.

  • A given defines a canonical instance of a type: given Ordering[Version] = Ordering.by(v => (v.major, v.minor)).
  • A using clause declares a parameter the compiler fills in from givens in scope: def sortAll[A](xs: List[A])(using ord: Ordering[A]). Callers write sortAll(versions), or pass one explicitly with sortAll(versions)(using reverseOrd).
  • A context bound is shorthand: def sortAll[A: Ordering](xs: List[A]), and summon[Ordering[A]] retrieves the instance.

Together these implement type classes: a trait describing a capability (trait Show[A] { def show(a: A): String }), given instances for each type, and generic functions requiring the capability. Unlike inheritance, you can add a capability to types you don't own (Int, java.time.Instant) and choose different instances in different scopes.

Where does the compiler look? In the lexical scope (including imports) and in the implicit scope of the involved types: companion objects of Show, of A, and of their parts. Put instances for your own types in their companions and they're found without imports. Importing givens needs the special form import mylib.given.

derives Show on a case class auto-generates an instance when the type class supports derivation.

Example

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

object Show:
  given Show[Int] with
    def show(n: Int) = s"#$n"
  given [A](using s: Show[A]): Show[List[A]] with
    def show(xs: List[A]) = xs.map(s.show).mkString("[", ", ", "]")

case class Version(major: Int, minor: Int)
object Version:
  given Ordering[Version] = Ordering.by(v => (v.major, v.minor))
  given Show[Version] = v => s"v${v.major}.${v.minor}"

def describe[A: Show](a: A): String = "value " + summon[Show[A]].show(a)

@main def tc(): Unit =
  println(describe(42))                        // value #42
  println(describe(List(1, 2)))                // value [#1, #2]
  val vs = List(Version(1, 10), Version(1, 2), Version(0, 9))
  println(vs.sorted.map(describe(_)))          // List(value v0.9, value v1.2, value v1.10)
  println(vs.max)                              // Version(1,10)
  val newestFirst = summon[Ordering[Version]].reverse
  println(vs.sorted(using newestFirst).head)   // Version(1,10)

Remember

  • given provides an instance; using requires one
  • Context bounds [A: Show] plus summon are shorthand
  • Type classes add capabilities to types you don't own
  • Companion objects are searched automatically; others need import x.given

Watch out for

  • Defining givens in an unrelated object and forgetting import thatObject.given; a normal wildcard import doesn't bring givens.

Check yourself

  1. Where should a given Ordering[Version] live so that it's found without imports?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scala-lang.org/scala3/book/ca-given-using-clauses.html