Reading — step 1 of 7
Learn
Scala 2's implicits are the most contested feature — beloved by libraries, confusing to newcomers. Scala 3 redesigned them as given/using/extension. This lesson covers BOTH so you can read modern and legacy code.
What implicits do
Three kinds:
- Implicit parameters — compiler fills in arguments from context
- Implicit conversions — auto-convert one type to another
- Implicit classes — extension methods on existing types
// 1. Implicit parameter
def greet(name: String)(implicit greeting: String): String = s"$greeting, $name"
implicit val hi: String = "Hi"
println(greet("Ada")) // "Hi, Ada" — `hi` filled in
// 2. Implicit conversion (Scala 2 — frowned upon now)
implicit def intToString(n: Int): String = s"number: $n"
val s: String = 42 // works because of conversion
// 3. Implicit class
implicit class IntOps(n: Int) {
def squared: Int = n * n
}
5.squared // 25 — extension method
Implicit resolution rules
When the compiler needs an implicit X, it searches in this order:
- Local scope —
val/defin the current method or enclosing scope - Imports —
import path.implicits._brings them into scope - The companion objects of any of the types involved (X itself, base classes, type parameters)
The FIRST match wins. Ambiguity is a compile error.
This is why type classes work — you put the instance in the companion object, and the compiler finds it without explicit import.
Type classes via implicits
trait Show[A] {
def show(a: A): String
}
object Show {
// Default instance — found via companion object lookup
implicit val intShow: Show[Int] = (a: Int) => s"int: $a"
implicit val strShow: Show[String] = (a: String) => s"str: '$a'"
}
def display[A](a: A)(implicit s: Show[A]): String = s.show(a)
display(42) // "int: 42"
display("hello") // "str: 'hello'"
// display(3.14) // ERROR — no Show[Double] in scope
Context bound — shorthand
def f[A: TC](a: A) is sugar for def f[A](a: A)(implicit ev: TC[A]):
def display[A: Show](a: A): String = implicitly[Show[A]].show(a)
implicitly[T] retrieves the implicit value of type T from scope.
Scala 3: given / using / extension
Same mechanics, cleaner syntax:
// Scala 2:
implicit val intShow: Show[Int] = (a: Int) => s"int: $a"
def display[A](a: A)(implicit s: Show[A]): String = s.show(a)
// Scala 3:
given Show[Int] = (a: Int) => s"int: $a"
def display[A](a: A)(using s: Show[A]): String = s.show(a)
// Extension (replaces implicit class):
extension (n: Int)
def squared: Int = n * n
The new keywords:
given— provides an implicit valueusing— requests an implicit parameterextension— adds methods to existing types
More explicit, less ambiguous than implicit everywhere.
Where implicits are essential
- Type classes (Show, Numeric, Ordering, Functor, Monad)
- Implicit conversions (rarely — modern style avoids these)
- Extension methods (StringOps, IntOps, RichInt)
- Context propagation (ExecutionContext, MDC for logging)
- Builders / DSL marking (db transactions with implicit Connection)
Where implicits go wrong
- Implicit conversions in user code — surprising auto-conversions. Use sparingly.
- Long implicit chains — debugging "why doesn't this compile?" gets hard. Use
-Xlog-implicits. - Ambiguous implicits — two equally-good candidates. Compile error; specify explicitly.
- Imports that pull in too much — avoid
import implicits._mass imports; be selective.
Common mistakes
- Forgetting
impliciton the value — compiler can't find it without the keyword. - Wrong scope — implicit in nested scope but used outside; or in wrong package.
- Implicit value doesn't match expected type — silently not found. Use
implicitly[T]to debug. - Mixing Scala 2 and Scala 3 syntax — pick one consistent with your project.
- Implicit conversions to common types like String — turns mismatched types silent. Avoid.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…