Skip to content

06 · Implicits Basics

"Implicits" is Scala's long-standing name for values and conversions the compiler can supply automatically, without you writing them out at every call site. Scala 3 gave the underlying mechanism clearer, dedicated keywords — given/using for implicit values and parameters, extension for adding methods to existing types — which is what this module teaches. You'll still see the older implicit val/implicit def/implicit class spellings in Scala 2 code and older library docs; they solve the same problems.

Implicit parameters: given and using

A using clause on a method marks a parameter the caller normally doesn't pass explicitly — the compiler looks for a matching given value in scope instead:

case class Config(prefix: String)

def logMessage(msg: String)(using config: Config): String =
  s"${config.prefix}$msg"

given defaultConfig: Config = Config("[app] ")

println(logMessage("started"))   // [app] started -- defaultConfig supplied automatically

Nothing at the call site mentions Config at all — the compiler found the one given Config in scope and threaded it through. This is how a lot of Scala library code passes cross-cutting context (an ExecutionContext for Future, a JSON encoder, an Ordering) without every call needing to repeat it.

The trap: which given wins, and when it's decided

Implicit resolution is lexical scoping, same as any other name — but given/implicit definitions behave like defs for ordering purposes, not like vals: a local given is visible to code earlier in the same block, not just code after it. This regularly surprises people coming from ordinary variable semantics:

case class Config(prefix: String)
def logMessage(msg: String)(using config: Config): String = s"${config.prefix}$msg"

given topLevel: Config = Config("[top] ")

@main def run(): Unit =
  println(logMessage("first"))          // you might expect "[top] first"...
  given local: Config = Config("[local] ")
  println(logMessage("second"))

// Actual output:
// [local] first
// [local] second

Both calls use local, even though it's declared after the first println — because within run's body, local is in scope for the whole block (like a method, not like a val, which would instead fail to compile with "forward reference extends over definition" if you tried the same trick with a plain val). The more specific, more local given also wins over the top-level one whenever both are visible. The practical lesson: don't assume implicit resolution respects the order code reads top-to-bottom — it respects scope, and a local given shadows an outer one for the entire enclosing block.

Implicit conversions

A given Conversion[A, B] tells the compiler it may automatically convert an A to a B wherever a B is expected. This is powerful and easy to overuse — reach for it sparingly, since silent type conversions can make code harder to reason about at a glance:

case class Meters(value: Double)
case class Feet(value: Double)

given Conversion[Meters, Feet] with
  def apply(m: Meters): Feet = Feet(m.value * 3.28084)

def needsFeet(f: Feet): String = s"${f.value} ft"

val m = Meters(10.0)
println(needsFeet(m))   // 32.8084 ft -- Meters silently converted to Feet

Prefer an explicit method (m.toFeet) for most conversions; reserve implicit conversions for narrow, well-understood cases (unit wrappers, adapting a third-party type to an interface you don't own) where the conversion is unambiguous and safe every time it fires.

Extension methods: adding methods to types you don't own

extension lets you add methods to an existing type — including types from the standard library — without subclassing or wrapping it:

extension (s: String)
  def shout: String = s.toUpperCase + "!"
  def isPalindrome: Boolean = s == s.reverse

println("hello".shout)          // HELLO!
println("level".isPalindrome)   // true
println("scala".isPalindrome)   // false

An extension can also take its own parameters or even a by-name block, which is how you'd build a small DSL-like helper:

extension (n: Int)
  def times(block: => Unit): Unit =
    var i = 0
    while i < n do
      block
      i += 1

3.times { print("hi ") }   // hi hi hi
println()

3.times { ... } reads like new syntax, but it's ordinary Scala: 3 is an Int, times is an extension method on Int that happens to accept a block. This is exactly the mechanism behind familiar-looking library one-liners like 5.seconds or "text".isBlank-style helpers you'll meet in third-party code.

Bringing it together: given-based dispatch

Combining given/using with a small trait gets you type-directed behavior — different logic runs depending on the type involved, decided at compile time by which given matches:

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

given Show[Int] with
  def show(a: Int): String = s"Int($a)"

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

def printIt[A](a: A)(using s: Show[A]): Unit =
  println(s.show(a))

printIt(42)        // Int(42)
printIt("hello")   // Str("hello")

This pattern — a trait describing a capability, plus one given instance per type that has that capability — is called a type class, and it's exactly how Module 7 · Working with JSON's encoders/decoders work under the hood. Level 3 covers type classes properly, including writing your own generic ones.

How It Actually Works

When the compiler sees a using parameter it can't find at the call site, it doesn't fail immediately — it searches an implicit scope built from the local scope outward, then the companion objects of every type involved in the parameter's type signature, collecting every given that type-checks as a candidate. If more than one candidate matches, specificity rules (a given in a narrower scope, or one whose type is a subtype of another candidate's) decide the winner at compile time — there's no runtime lookup at all, the compiler bakes in a direct reference to the winning value, which is why an ambiguous-implicit error is a compile failure, not a runtime one. Extension methods work by the same resolution mechanism: obj.method() where method isn't defined on obj's type triggers the compiler to search for an extension whose receiver type matches, then rewrites the call as if you'd written the extension's defining function directly.

Cheat sheet

Scala 3 keyword Old (Scala 2) name Purpose
given (value) implicit val/implicit object Provide a value the compiler can inject
using (parameter) implicit parameter Mark a parameter to be filled in automatically
given Conversion[A, B] with ... implicit def (A => B) Automatic type conversion
extension (x: T) def foo = ... implicit class Add a method to an existing type

Exercise

Define case class Distance(km: Double). Write an extension on Distance adding def miles: Double (1 km ≈ 0.621371 miles). Separately, define a trait Describable[A] with def describe(a: A): String, then write two given Describable[...] instances — one for Int (e.g. "the number N") and one for your Distance (e.g. "N km (M miles)", reusing your miles extension) — and a generic def report[A](a: A)(using d: Describable[A]): Unit = println(d.describe(a)). Call report with both an Int and a Distance to confirm the right given is picked for each type.