Scala 3 given/using: implicit parameters, renamed and rebuilt

The given / using rework in Scala 3 isn't just a syntax swap. It's a genuine redesign of how implicit context works in the language — driven by a decade of Scala 2 pain around implicit scope, derivation, and accidental ambiguity. I've been using it on a real codebase for a few months; here's what holds up and what I had to relearn.

The core rename

Scala 2:

def greet(name: String)(implicit greeting: Greeting): String =
  s"${greeting.text}, $name!"

implicit val english: Greeting = Greeting("Hello")

Scala 3:

def greet(name: String)(using greeting: Greeting): String =
  s"${greeting.text}, $name!"

given Greeting = Greeting("Hello")

Two things changed. First, using at the call site is distinct from implicit, so you can read a signature and immediately tell "this parameter is passed implicitly" without the word being overloaded with six other meanings. Second, given replaces implicit val/implicit def with a dedicated keyword whose only job is to provide contextual values.

Why this matters

In Scala 2, the keyword implicit did four different jobs: declaring implicit parameters, declaring implicit conversions, declaring typeclass instances, and enabling extension methods. Scala 3 gave each of these its own syntax:

  • using for implicit parameters at use sites.
  • given for providing values.
  • extension for adding methods to a type.
  • Conversion[A, B] as an explicit typeclass for coercions — no more mysterious implicit def that could fire anywhere.

The split makes code immediately more readable. You can look at a file and see exactly what is being added to the scope and in what role.

Anonymous givens

One pattern I use a lot is anonymous givens — no name required:

given Greeting = Greeting("Hello")
given Codec[User] = Codec.derived[User]

In Scala 2, you had to come up with names for these (implicit val defaultGreeting, implicit val userCodec), which led to a kind of fake-namespace cargo cult. Anonymous givens admit that most typeclass instances are uniquely identified by their type, and that picking a name for them is usually fiction.

Context bounds, upgraded

Context bounds in Scala 3 work the same as in 2 at the syntactic level — def f[A: Ord] still means "take an implicit Ord[A]". But they now compose with using parameters naturally, and you can reference the given via summon[Ord[A]] instead of the old implicitly trick. Small change, but summon reads better and makes type inference cleaner in more edge cases.

The things I still trip on

Importing givens. Unlike Scala 2, given values are not imported by a regular import foo._. You need either import foo.given or the specific syntax import foo.{given Greeting}. This tripped me up for weeks — perfectly valid givens in scope, refusing to resolve, because I was writing import statements the old way. Once I got it, it started to feel like a feature: givens don't leak as accidentally as implicits did.

Derivation ergonomics. Typeclass derivation in Scala 3 is dramatically better than the Magnolia dance we did in Scala 2, but there's still a learning curve around Mirror, inline given, and when to use derives on the class versus declaring the derivation separately. Worth learning — it unlocks things that were painful to express before — but it's not trivial.

Verdict

A clear win. The rename alone would have been useful; the semantic cleanup on top of it makes Scala 3 feel like a language where the implicit machinery was designed with the pitfalls of Scala 2 specifically in mind. Migration has small frictions, but nothing that wasn't worth the payoff after a few weeks.