Context Functions
|
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, released 2026-10-12) and every example was compiled and run with Scala 3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). Context functions have been part of Scala 3 since 3.0 and have not changed in the syntax since; the runnable examples were also compiled and run on the 3.9 LTS (3.9.0, 2026-09-03) with the same output, and the builder example on the 3.3 LTS (3.3.8) as well.
A context function is a function whose only parameters are context parameters: where a normal function type
A => B takes an argument you pass, the context function type A ?=> B takes an argument the compiler fills in from
the givens in scope, in the same way as it does for a method with a using clause. The arrow ?=> marks the
difference. Context functions are how a library hands a block of code a "current" object (a table being built, a logger,
a transaction) without every call in the block having to name it, so they are the engine behind nested builders and
internal DSLs. They build on Givens & Using Clauses and
Contextual Abstractions Overview. The examples use the
trailhead scenario, a toolkit that plans hikes on a network of mountain trails.
Context function types and literals
A context function type is written with ?=>, usually behind a type alias that names the capability. A method can return
one, and a parameter can have one as its type:
//> using scala 3.10.0
package trailhead
case class Logger(prefix: String):
def info(msg: String): Unit = println(s"[$prefix] $msg")
type Logged[T] = Logger ?=> T
def start(trail: String): Logged[Int] =
summon[Logger].info(s"starting $trail")
trail.length
def twice(body: Logged[Unit]): Logged[Unit] =
body
body
def runWith[T](prefix: String)(body: Logged[T]): T =
body(using Logger(prefix))
@main def run(): Unit =
given Logger = Logger("main")
println(start("Ridge")) // argument inferred
println(start("Ridge")(using Logger("tmp"))) // explicit argument
runWith("outer") { start("Pico") }
// an expression is converted to a context function literal
val a: Logged[Int] = 10 + start("Arch")
println(a) // applied to the given in scope
val f: Logged[Int] = (log: Logger) ?=> start("Cave")(using log)
println(f(using Logger("explicit")))
twice { summon[Logger].info("hi") }
[main] starting Ridge
5
[tmp] starting Ridge
5
[outer] starting Pico
[main] starting Arch
14
[explicit] starting Cave
4
[main] hi
[main] hi
The example shows the rules that the reference gives:
-
A reference to a value of context function type is applied to a synthesized argument as soon as it is used, exactly as a method with a
usingclause would be:start("Ridge")finds theLoggergiven in scope, andstart("Ridge")(using Logger("tmp"))passes one explicitly. -
Conversely, when the expected type of an expression
Eis a context function type(T1, ..., Tn) ?=> UandEis not already a context function literal, the compiler rewrites it to(x1: T1, ..., xn: Tn) ?=> Ebefore type checkingE. The names are arbitrary, and the parameters are available as givens insideE. That is why10 + start("Arch")can be aLogged[Int]and whytwice { summon[Logger].info("hi") }finds theLoggerof whichever context ends up running the block. -
A context function literal is written with the same arrow,
(log: Logger) ?=> body. The parameter is a given in the body; callf(using Logger("explicit"))to apply a stored function explicitly. -
runWithcallsbody(using Logger(prefix))to discharge the context function, which is the way a library supplies the "current" value to a block.
In a context function type, parameters have no names in the type (Logger ?=> T), but may have names when the result
depends on them, as shown with dependent function types below.
What the compiler generates
Seeing the expansion removes most of the mystery. The program below has a day block that supplies a Day; each stop
inside needs a Day as a using parameter. Compiled with -Xprint:typer, which prints the trees after type checking,
the block becomes a lambda with a using parameter (the printout is trimmed to the relevant part):
//> using scala 3.10.0
//> using option -Xprint:typer
package trailhead
class Day:
def add(name: String): Unit = println(name)
def day(init: Day ?=> Unit): Unit = init(using Day())
def stop(name: String)(using d: Day): Unit = d.add(name)
@main def run(): Unit =
day:
stop("Pico")
stop("Arch")
def day(init: (trailhead.Day) ?=> Unit): Unit =
init.apply(using new trailhead.Day())
def stop(name: String)(using d: trailhead.Day): Unit = d.add(name)
@main def run(): Unit =
trailhead.day((using contextual$1: trailhead.Day) =>
{
trailhead.stop("Pico")(contextual$1)
trailhead.stop("Arch")(contextual$1)
}
)
The block passed to day has been wrapped in a context function literal with a compiler-generated parameter
(contextual$1), and every stop call receives it as its using argument. After erasure, a context function is a plain
Function1 (for one parameter); a context function type is a compile-time concept, so a method that takes
Day ?=> Unit is, to Java and to the JVM, a method that takes a Function1.
The builder pattern
The headline use is the nested builder. The aim is a block structure in which an inner call implicitly belongs to the nearest enclosing block, with no explicit argument threading:
//> using scala 3.10.0
package trailhead
import scala.collection.mutable.ArrayBuffer
class Itinerary:
val days = ArrayBuffer[Day]()
def add(d: Day): Unit = days += d
override def toString = days.mkString("Itinerary(", ", ", ")")
class Day:
val stops = ArrayBuffer[Stop]()
def add(s: Stop): Unit = stops += s
override def toString = stops.mkString("Day(", ", ", ")")
case class Stop(name: String, km: Double)
def itinerary(init: Itinerary ?=> Unit): Itinerary =
given it: Itinerary = Itinerary()
init
it
def day(init: Day ?=> Unit)(using it: Itinerary): Unit =
given d: Day = Day()
init
it.add(d)
def stop(name: String, km: Double)(using d: Day): Unit =
d.add(Stop(name, km))
@main def run(): Unit =
val plan = itinerary:
day:
stop("Refugio", 0.0)
stop("Pico", 8.5)
day:
stop("Arch", 3.2)
println(plan)
// stop("lost", 1.0) // outside a day block: no Day given
Itinerary(Day(Stop(Refugio,0.0), Stop(Pico,8.5)), Day(Stop(Arch,3.2)))
itinerary, day and stop have the shape recommended by the reference: the outer functions take a context function
(Itinerary ?=> Unit, Day ?=> Unit), create a fresh instance as a given and run the block with it in scope, and the
inner functions ask for the instance with a using parameter. The colon syntax (day:) is the braceless way of passing
the block; see Optional Braces & Indentation.
The same builder is the subject of Domain Specific Languages;
the Kotlin counterpart uses lambdas with receivers and is described in
Type-safe Builders & DSLs.
Because the dependency is a context parameter, using a builder call in the wrong place is a compile error rather than a run-time failure:
//> using scala 3.10.0
package trailhead
class Day:
def add(name: String): Unit = println(name)
def day(init: Day ?=> Unit): Unit = init(using Day())
def stop(name: String)(using d: Day): Unit = d.add(name)
@main def run(): Unit =
stop("lost")
./b_err.scala:11:15
No given instance of type trailhead.Day was found for parameter d of method stop in package trailhead
stop("lost")
^
When blocks are nested, the innermost given wins; there is no ambiguity, because the given in the inner lambda shadows the outer one:
//> using scala 3.10.0
package trailhead
case class Day(n: Int)
def day(n: Int)(init: Day ?=> Unit): Unit = init(using Day(n))
@main def run(): Unit =
day(1):
day(2):
println(summon[Day]) // the innermost given wins
println(summon[Day])
Day(2)
Day(1)
A consequence for DSL design is that the inner block can still reach the outer one only if it is explicitly named. Give
each level its own type (Itinerary, Day), as above, so the inner call asks for exactly the level it needs.
Postconditions
The second example in the reference combines three features to get a zero-overhead abstraction: an opaque type that makes the given unique, a context function type as the parameter, and an extension method so that the block can use a plain name.
//> using scala 3.10.0
package trailhead
object PostConditions:
opaque type WrappedResult[T] = T
def result[T](using r: WrappedResult[T]): T = r
extension [T](x: T)
def ensuring(condition: WrappedResult[T] ?=> Boolean): T =
assert(condition(using x))
x
end PostConditions
import PostConditions.{ensuring, result}
def totalKm(legs: List[Double]): Double =
legs.sum.ensuring(result > 0 && result < 100)
@main def run(): Unit =
println(totalKm(List(8.5, 3.25, 4.25)))
val s = List(1, 2, 3).sum.ensuring(result == 6)
println(s)
try List(1, 2, 3).sum.ensuring(result == 7)
catch case e: AssertionError => println(s"failed: ${e.getMessage}")
16.0
6
failed: assertion failed
WrappedResult[T] is an opaque alias of T, so no wrapper object exists at run time. It is a fresh type on purpose: a
using parameter of type Double would let any Double given in scope be picked up by accident, and the reference
recommends a fresh type whenever context parameters are involved. The condition result > 0 && result < 100 is a
WrappedResult[Double] ?=> Boolean, so the compiler passes the given along to result. See
Opaque Types & Value Classes and
Extension Methods. The failure message is the plain
AssertionError of assert; a real library would give it a better message.
Several context parameters and library uses
A context function may have several parameters, (A, B) ?=> C, which the compiler fills in as one unit. A type alias keeps
signatures short. The standard library itself uses a context function type: scala.util.boundary takes a block of type
Label[T] ?=> T, and boundary.break asks for the label with a using parameter. See
Exceptions, boundary & Early Exit.
//> using scala 3.10.0
package trailhead
import scala.util.boundary
import scala.util.boundary.Label
case class Config(units: String)
case class Clock(hour: Int)
// several context parameters: (A, B) ?=> C
type Planned[T] = (Config, Clock) ?=> T
def describe(km: Double): Planned[String] =
s"$km ${summon[Config].units} at ${summon[Clock].hour}h"
// the standard library uses the same idea: boundary[T](body: Label[T] ?=> T)
def firstLong(legs: List[Double]): Option[Double] =
boundary:
for l <- legs do
if l > 10 then boundary.break(Some(l))
None
// nested givens of the same type: the innermost wins
def nested(): Unit =
given Config = Config("km")
def inner(using c: Config) = c.units
println(inner)
println(inner(using Config("mi")))
val g: Planned[String] = describe(2.0)
println(g(using Config("m"), Clock(7)))
@main def run(): Unit =
nested()
println(firstLong(List(3.0, 12.5, 40.0)))
println(firstLong(List(3.0)))
km
mi
2.0 m at 7h
Some(12.5)
None
Source: Scala API: scala.util.boundary
The Planned[String] value g is applied with two using arguments at once, in the order of the type. In nested, the
inner(using Config("mi")) call shows that a given already in scope can always be overridden by passing an explicit
argument.
Combination with dependent function types
Context functions combine with dependent function types,
where the result or a later parameter type refers to an earlier parameter. The Scala 3 Book builds a small numbers DSL this
way: an alias of the form (n: NumsDSL) ?=> n.Num => n.Num is a program that implicitly assumes a NumsDSL and whose
number type is that DSL’s. The same idea for the trailhead scenario, with distances:
//> using scala 3.10.0
package trailhead
trait Metrics:
type Dist
def km(d: Double): Dist
def plus(a: Dist, b: Dist): Dist
def show(d: Dist): String
trait MetricsDSL extends Metrics:
extension (a: Dist) def +(b: Dist): Dist = plus(a, b)
def const(d: Double)(using m: Metrics): m.Dist = m.km(d)
object Plain extends MetricsDSL:
type Dist = Double
def km(d: Double) = d
def plus(a: Double, b: Double) = a + b
def show(d: Double) = s"$d km"
// a context function whose parameter type depends on the context argument
type Adjust = (m: MetricsDSL) ?=> m.Dist => m.Dist
// a context function whose result type depends on its own context parameter
type Route = (m: MetricsDSL) ?=> m.Dist
def apply(base: Double, adjust: Adjust): String =
Plain.show(adjust(using Plain)(Plain.km(base)))
def total(r: Route): String =
Plain.show(r(using Plain))
@main def run(): Unit =
println(apply(8.5, d => d + const(1.5)))
println(total(const(8.5) + const(3.25)))
10.0 km
11.75 km
Adjust is a context function whose parameter, m: MetricsDSL, determines the type of the function that follows it
(m.Dist => m.Dist), and Route is a context function whose result type is m.Dist. The caller writes d => d + const(1.5) or
const(8.5) + const(3.25) without ever mentioning the MetricsDSL instance, and the library chooses which implementation
runs the program. That is a lightweight form of the "tagless final" style: the program is written against an abstract
algebra and the implementation is supplied when it is run. The extension + comes from the MetricsDSL given found by
the compiler.
Pitfalls
-
A stored context function is applied when used. A value of type
Config ?=> Stringbehaves like a method with ausingclause. Calling a method on it asks for the given immediately, so a given has to be in scope or the call must be written with(using ...):
//> using scala 3.10.0
package trailhead
case class Config(units: String)
def label(km: Double): Config ?=> String = s"$km ${summon[Config].units}"
@main def run(): Unit =
val pending: Config ?=> String = label(2.0)
println(pending.length)
./j_err.scala:10:18
No given instance of type trailhead.Config was found for parameter of (trailhead.Config) ?=> String
println(pending.length)
^
-
The fix is to supply the given, either explicitly or by importing or defining one:
//> using scala 3.10.0
package trailhead
case class Config(units: String)
def label(km: Double): Config ?=> String = s"$km ${summon[Config].units}"
@main def run(): Unit =
val metric = Config("km")
val pending: Config ?=> String = label(2.0)
// supply the given explicitly, or bring one into scope
println(pending(using metric).length)
given Config = metric
println(pending.length)
6
6
-
Accidental givens. A context parameter of a common type (
Int,String) picks up any given of that type that happens to be in scope. Wrap the dependency in its own type or an opaque alias, asWrappedResultdoes. -
Erasure. After erasure a context function is a
Function1, so two overloads that differ only in the parameter of a context function type have the same erased signature and clash:
//> using scala 3.10.0
package trailhead
def run(body: String ?=> Int): Int = body(using "a")
def run(body: Long ?=> Int): Int = body(using 1L)
./l_err.scala:5:5
Conflicting definitions:
def run(body: (String) ?=> Int): Int in the top-level definitions in package trailhead at line 4 and
def run(body: (Long) ?=> Int): Int in the top-level definitions in package trailhead at line 5
have the same type (body: Function1): Int after erasure.
Consider adding a @targetName annotation to one of the conflicting definitions
for disambiguation.
def run(body: Long ?=> Int): Int = body(using 1L)
^
-
The compiler suggests
@targetNameon one of them; different method names are usually clearer. -
Allocation. A context function that is not inlined typically allocates a closure for the block, as an ordinary lambda does. For hot paths,
inlinemethods and plainusingparameters avoid it; see Inline. -
Literals are not calls. A context function literal is a value, not a call:
val n: Int = (s: String) ?=> 3is rejected with "Found: (String) ?⇒ Int, Required: Int".
Comparison with other languages
-
Scala 2. There are no context function types. Builders passed the object explicitly, kept it in mutable state or threaded
implicitparameters by hand. In the early Scala 3 work the feature was called "implicit function types" and had a different syntax; the current one isA ?=> B. -
Java. Lambdas have no receiver or implicit parameter, so builders pass the object to the lambda (
table(t -> t.row(...))) or rely on a fluent method chain. -
Kotlin. Lambdas with receivers (
Table.() -> Unit) provide the same ergonomics: the receiver is in scope in the block and@DslMarkerlimits access to outer receivers. A Scala context function is more general, since it can request any number of givens and can carry a dependent type, and the lookup is by type: the nearest given of the requested type wins, so each level of a builder should have its own type. There is no equivalent of@DslMarkerthat forbids reaching an outer level; the type system only picks the innermost one. See Type-safe Builders & DSLs.
Scala 3.9 LTS versus 3.10
There is no difference for context functions between 3.9.0 and 3.10.0: the examples here compile and print the same output
on both, and the builder example also does on the 3.3 LTS. What changed in recent releases is the surrounding syntax: since
Scala 3.6 a conditional given is written given [T: Ord] => Ord[List[T]] rather than with with, which affects how the
givens that feed context functions are declared, not the context functions themselves.
Where the books are outdated
-
The blog article linked from the reference, Simplicitly: foundations and applications of implicit function types, uses the older
implicit A => Bsyntax. The current syntax isA ?=> B. -
Older tutorials and the early Dotty documentation call them "implicit function types".
-
Texts that declare givens with the
withkeyword (for examplegiven ec: ExecutionContext with ...) use the legacy form; the Scala 3.6 syntax is described in Givens & Using Clauses.