Contextual Abstractions Overview
|
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, 2026-10-12). Every example was run with Scala CLI on JDK 26 on
2026-10-15: the Scala 3 examples on Scala 3.10.0, the Scala 2 examples on Scala 2.13.18. The syntax described here as
"the 3.6 syntax" was introduced experimentally in 3.5.0 and stabilised in 3.6 (SIP-64; 3.6.0 and 3.6.1 were withdrawn,
so 3.6.2 was the first release to use). It therefore works on the 3.9 LTS but not on the 3.3 LTS line, which has the
older with and (using ...) forms; this is called out wherever it matters.
Contextual abstraction is the umbrella term for everything that lets the compiler supply a value, a conversion or an
extra method that the programmer did not write at the call site, because it is determined by the context: the types
involved and the definitions that are in scope. In Scala 2 all of it was spelled with one modifier, implicit. In
Scala 3 each use case has its own construct. This page explains why, lists the constructs, shows the mapping from Scala 2,
and points to the page where each construct is covered in depth. The examples use the trailhead scenario of this
section: a toolkit that plans hikes on a network of mountain trails.
Why implicits were redesigned
Scala 2’s implicits are, in the words of the Scala 3 Reference, "at the same time Scala’s most distinguished and most controversial feature". The design is powerful: an implicit parameter leads to an inferred argument term that could also be written explicitly, which covers type classes, establishing a context, dependency injection, capabilities, and computing types. The Reference lists these criticisms of how that power was packaged:
-
Over-use and mis-use, especially of implicit conversions. A conditional implicit value (
implicit def i1(implicit x: T): C[T]) and an implicit conversion (implicit def i2(x: T): C[T]) differ by one parameter-list keyword. Conditional values are the cornerstone of type classes, while most uses of conversions turned out to be of dubious value, yet newcomers start with conversions because they are easy to understand. -
Over-reliance on implicit imports. Inscrutable type errors "go away with the right import incantation", and it is hard to see which implicits a program uses because they can hide in a long list of imports.
-
The syntax conveys mechanism, not intent. The single
implicitmodifier is attached toval,def,object,classand parameters. A type class instance is animplicit valorobjectwhen unconditional and animplicit defwith implicit parameters when conditional, and the cues for intent are indirect. -
Implicit parameters and their arguments look alike. A call such as
currentMap("a")passes"a"to the implicit parameter, so one has to writecurrentMap.apply("a"). Only one implicit parameter section is allowed, and it must be last. Implicit parameters must be named even when the name is never used. -
Tooling. Which implicits are available depends on the context, so completion needs the context, and a failed search deep in a recursion gave unspecific error messages (the Scala 3 compiler has since improved this and suggests imports).
The Reference concludes that none of the shortcomings is fatal, but that together they make implicit-heavy code "a lot
more cumbersome and less clear than it could be", and that a radically new design is justified for newcomers. The
"New Design" has four fundamental changes: given instances replace implicit definitions, using clauses replace
implicit parameter lists, given imports are a new class of import selectors, and implicit conversions become instances
of the standard Conversion class. The same chapter adds extension methods, context bounds, type class derivation,
multiversal equality and context function types.
The same idea in one example, first in Scala 2 and then in Scala 3. Both programs do the same thing: a Show type
class with an instance for kilometres, a conditional instance for lists, an enrichment method shown, and a conversion
from Double to Km.
//> using scala 2.13.18
import scala.language.implicitConversions
case class Km(value: Double)
trait Show[A] { def show(a: A): String }
object Show {
implicit val kmShow: Show[Km] = new Show[Km] { def show(k: Km) = s"${k.value} km" }
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 Syntax {
implicit class ShowOps[A](val a: A) extends AnyVal {
def shown(implicit s: Show[A]): String = s.show(a)
}
implicit def doubleToKm(d: Double): Km = Km(d)
}
object Main extends App {
import Syntax._
def render[A](a: A)(implicit s: Show[A]): String = s.show(a)
println(render(List(Km(1), Km(2.5))))
println(List(Km(3)).shown)
val k: Km = 4.0
println(k)
println(implicitly[Show[Km]].show(k))
}
[1.0 km, 2.5 km]
[3.0 km]
Km(4.0)
4.0 km
//> using scala 3.10.0
import scala.language.implicitConversions
case class Km(value: Double)
trait Show[A]:
def show(a: A): String
object Show:
given Show[Km] = k => s"${k.value} km" // was: implicit val
given [A] => (s: Show[A]) => Show[List[A]] = // was: implicit def with implicit parameter
xs => xs.map(s.show).mkString("[", ", ", "]")
extension [A](a: A) // was: implicit class
def shown(using s: Show[A]): String = s.show(a)
given Conversion[Double, Km] = Km(_) // was: implicit def doubleToKm
def render[A](a: A)(using s: Show[A]): String = s.show(a) // was: (implicit s: Show[A])
@main def s3(): Unit =
println(render(List(Km(1), Km(2.5))))
println(List(Km(3)).shown)
val k: Km = 4.0
println(k)
println(summon[Show[Km]].show(k)) // was: implicitly
[1.0 km, 2.5 km]
[3.0 km]
Km(4.0)
4.0 km
The behaviour is identical; what changed is that every definition now says what it is. The given [A] => (s: Show[A]) =>
line reads as "given, for every A for which a Show[A] exists, a `Show[List[A]]`".
The constructs and where they are covered
| Construct | What it expresses | Page |
|---|---|---|
|
A term the compiler may synthesize for a type: a type class instance, a context value, a capability |
|
|
A parameter list whose arguments are inferred, and the syntax to pass them explicitly with |
|
|
An import selector that imports givens and nothing else |
|
Context bound |
Shorthand for a |
|
|
Methods added to an existing type, replacing the implicit class idiom |
|
|
An implicit conversion, now an explicit and flag-controlled given instance |
|
Context function |
A function type whose parameter is a |
|
|
Compiler-generated given instances of a type class for a type |
The type class pattern, which is the main application of all of these, is the subject of Type Classes.
Mapping Scala 2 to Scala 3
The diagram shows the replacement for each Scala 2 form. The table adds the details that decide how to migrate.
| Scala 2 | Scala 3 | Notes |
|---|---|---|
|
|
Alias givens are evaluated lazily; the Reference suggests a regular |
|
|
Conditional given. The conditions are context parameters, not ordinary parameters. |
|
|
The parameter name is optional; a method can have several |
|
|
The explicit argument is marked, so |
|
|
|
|
|
An implicit class used only to add methods becomes extensions; other uses become a class plus a |
|
|
Applying a conversion needs |
|
|
Scala 2 |
Implicit function type |
|
See Context Functions. |
Parameters and arguments no longer look alike
This is the second criticism in the list above, shown with real output. In Scala 2 the argument list after an implicit parameter is always the implicit list, so passing a regular argument is a type error:
//> using scala 2.13.18
object M1 extends App {
class Context { def lookup: Map[String, Int] = Map("a" -> 1) }
def currentMap(implicit ctx: Context): Map[String, Int] = ctx.lookup
implicit val ctx: Context = new Context
println(currentMap("a"))
}
[error] ./m1b.scala:6:22
[error] type mismatch;
[error] found : String("a")
[error] required: M1.Context
[error] println(currentMap("a"))
[error] ^^^
In Scala 3 currentMap("a") applies the result of currentMap, because arguments to a using clause must be marked with
using. A method can also have a regular parameter list between two using clauses, which implicit did not allow:
//> using scala 3.10.0
class Context:
def lookup: Map[String, Int] = Map("a" -> 1)
def currentMap(using ctx: Context): Map[String, Int] = ctx.lookup
given Context = Context()
def scaled(using ctx: Context)(key: String)(using factor: Int): Int = ctx.lookup(key) * factor
given Int = 10
@main def m2(): Unit =
println(currentMap("a"))
println(scaled("a"))
println(scaled("a")(using 3))
1
10
3
Source: Scala 3 Reference: Using Clauses
How the 3.6 syntax changed the picture
The first Scala 3 releases (3.0 to 3.5) introduced given and using, but spelled conditional and structured givens
with with and an extra (using ...) clause. SIP-64, experimental in 3.5.0 and stabilised in 3.6, replaced that
spelling. The goal, in the words of the 3.6.2 announcement, was "to simplify and narrow the syntax rules required to
create a given instance". The visible changes:
| Form | Before 3.6 (works on 3.3 LTS) | Since 3.6 (works on 3.9 LTS and 3.10) |
|---|---|---|
Given with a body |
|
|
Conditional given |
|
|
Context bounds |
|
|
By-name given |
not expressible as a given |
|
Abstract given in a trait |
|
|
The two spellings denote the same thing. On 3.10.0 the old form still compiles by default. This is the same conditional given in the 3.3 spelling, compiled on 3.10.0 without flags:
//> using scala 3.10.0
trait Ord[T]:
def compare(a: T, b: T): Int
given Ord[Int] with
def compare(a: Int, b: Int) = a.compare(b)
given [T](using Ord[T]): Ord[List[T]] with
def compare(a: List[T], b: List[T]) = a.size.compare(b.size)
@main def g7(): Unit =
println(summon[Ord[List[Int]]].compare(List(1), List(1, 2)))
-1
With -source:future the same file is rejected, which shows the direction of travel:
[error] ./h1.scala:5:16
[error] Given member definitions starting with `with` are no longer supported; use `{...}` or `:` followed by newline instead
[error] given Ord[Int] with
[error] ^
[error] ./h1.scala:7:7
[error] This old given syntax is no longer supported; use `=>` instead of `:`
[error] given [T](using Ord[T]): Ord[List[T]] with
[error] ^
Libraries that cross-build with the 3.3 LTS therefore keep the with spelling for now. The 3.6-style spelling is rejected by
3.3.8; for example given [T: Ord] => Ord[List[T]]: fails there with "':' expected, but '⇒' found".
Using Scala 2 code from Scala 3
Scala 3 compiles implicit definitions: "support for Scala 2’s implicits is an essential part of the common language
subset between 2.13 and Scala 3", in the Reference’s words. On 3.10.0 this compiles and runs without warnings:
//> using scala 3.10.0
trait Show[A] { def show(a: A): String }
object Show {
implicit val intShow: Show[Int] = new Show[Int] { def show(i: Int) = s"#$i" }
}
object Main {
def render[A](a: A)(implicit s: Show[A]): String = s.show(a)
def main(args: Array[String]): Unit = println(render(5))
}
#5
Three practical consequences:
-
Mixed code works in both directions. A
usingparameter is satisfied by animplicit val, and animplicitparameter is satisfied by agiven. This was run on 3.10.0: withimport Lib.*(animplicit val) andimport New.given(agiven) in scope, both ausingmethod and animplicitmethod found both instances and printed#1,'x'and#2. A Scala 3 library compiled withusingclauses can also be used from Scala 2.13 code, which sees them as implicit parameter lists (this is the Reference’s "common language subset"; it was not run here). -
import Instances.*importsimplicitdefinitions but not givens. Animplicit valimported with*satisfies ausingparameter; agivendoes not (see Givens & Using Clauses). Migrating an object of instances fromimplicittogiventherefore forces its importers to writeimport Instances.given. -
-source:futurerejects the old forms. Under that flag,implicitparameters andimplicit valdefinitions are errors with a message that shows the replacement; the compiler can rewrite parameters with-source:future-migration -rewrite:
[error] ./s3c.scala:8:23
[error] `implicit` parameters are no longer supported, use a `using` clause instead
[error] This construct can be rewritten automatically under -rewrite -source future-migration.
[error] def render[A](a: A)(implicit s: Show[A]): String = s.show(a)
[error] ^
[error] ./s3c.scala:5:16
[error] `implicit` vals are no longer supported, use a `given` clause instead. Example:
[error]
[error] given intShow: Show[Int] = ...
Running the rewrite on that file with //> using option -source:future-migration -rewrite changed only the parameter
list (def render[A](a: A)(implicit s: Show[A]) became def render[A](a: A)(using s: Show[A])) and left the
implicit val untouched. The rewrite is a first step, not a complete conversion: implicit val, implicit def and
implicit class definitions still have to be converted by hand.
Conversions need a flag
Of all the constructs, conversions are the one the redesign deliberately made noisy. A Conversion given is allowed,
but using it implicitly is a feature that must be enabled:
//> using scala 3.10.0
//> using option -feature
given Conversion[Int, String] = _.toString
@main def s3f(): Unit =
val s: String = 42
println(s.length)
[warn] ./s3f.scala:6:19
[warn] Use of implicit conversion given instance given_Conversion_Int_String should be enabled
[warn] by adding the import clause 'import scala.language.implicitConversions'
[warn] or by setting the compiler option -language:implicitConversions.
[warn] See the Scala docs for value scala.language.implicitConversions for a discussion
[warn] why the feature should be explicitly enabled.
[warn] val s: String = 42
[warn] ^^
The program then runs and prints 2. Without -feature the compiler prints only a one-line "feature warning" summary.
Conversions get their own page: Conversions, into & Implicit Resolution.
Comparison with other languages
-
Java has no equivalent. Values that Scala passes implicitly are passed explicitly, or injected by a framework at run time; the compiler never synthesizes an argument from a type. Extension of an existing class is not possible either.
-
Kotlin has extension functions (see Extension Functions & Scope Functions) and a feature called context parameters, but no counterpart of the global, type-directed search that finds a type class instance from the companion of the type: instances in Kotlin are passed or scoped by hand.
-
Haskell type classes and Rust traits are the closest relatives of
givenplus a type class trait. In both, there is one global instance per type (coherence), whereas Scala resolves instances from the lexical scope and the implicit scope, so several can exist and the one in the nearest scope wins; see the resolution rules on the next page. -
Python and JavaScript have no such feature;
functools.singledispatchor duck typing play part of the role.
Where the books are outdated
Books written for Scala 2, and the first generation of Scala 3 books, differ from current Scala 3 in these points:
-
Scala 2 books show only
implicit. Everything in the first table of this page is the Scala 3 replacement;implicitstill compiles but is rewritten by migration tooling and rejected under-source:future. -
Early Scala 3 material uses
withfor given bodies and(using ...)for conditional givens. Since 3.6 the recommended form is:and=>. Code and blog posts from 3.0 to 3.5 are still correct on the 3.3 LTS line. -
The Scala 3 Book’s introduction still describes the redesign accurately, but it predates SIP-64, so its examples use the older
withspelling in places. The semantics are unchanged. -
Implicit classes are described as the way to add methods; in Scala 3 that is
extension. -
implicitlyis shown as the way to retrieve an instance; in Scala 3 prefersummon.
The run results above are from 2026-10-15 on Scala 3.10.0 and Scala 2.13.18. The with-form example was also run on
the 3.3.8 LTS (it prints -1); the 3.6-style forms were run on 3.9.0 and 3.10.0.