Context Functions
Context functions are functions with (only) context parameters. Their types are context function types. Here is an example of a context function type:
type Executable[T] = ExecutionContext ?=> T
Context function are written using ?=>
as the "arrow" sign.
They are applied to synthesized arguments, in
the same way methods with context parameters are applied. For instance:
given ec as ExecutionContext = ...
def f(x: Int): Executable[Int] = ...
f(2)(using ec) // explicit argument
f(2) // argument is inferred
Conversely, if the expected type of an expression E
is a context function type
(T_1, ..., T_n) ?=> U
and E
is not already an
context function literal, E
is converted to an context function literal by rewriting to
(using x_1: T1, ..., x_n: Tn) => E
where the names x_1
, ..., x_n
are arbitrary. This expansion is performed
before the expression E
is typechecked, which means that x_1
, ..., x_n
are available as givens in E
.
Like their types, context function literals are written using ?=>
as the arrow between parameters and results. They differ from normal function literals in that their types are context function types.
For example, continuing with the previous definitions,
def g(arg: Executable[Int]) = ...
g(22) // is expanded to g((using ev: ExecutionContext) => 22)
g(f(2)) // is expanded to g((using ev: ExecutionContext) => f(2)(using ev))
g((using ctx: ExecutionContext) => f(22)(using ctx)) // is left as it is
Example: Builder Pattern
Context function types have considerable expressive power. For instance, here is how they can support the "builder pattern", where the aim is to construct tables like this:
table {
row {
cell("top left")
cell("top right")
}
row {
cell("bottom left")
cell("bottom right")
}
}
The idea is to define classes for Table
and Row
that allow
addition of elements via add
:
class Table {
val rows = new ArrayBuffer[Row]
def add(r: Row): Unit = rows += r
override def toString = rows.mkString("Table(", ", ", ")")
}
class Row {
val cells = new ArrayBuffer[Cell]
def add(c: Cell): Unit = cells += c
override def toString = cells.mkString("Row(", ", ", ")")
}
case class Cell(elem: String)
Then, the table
, row
and cell
constructor methods can be defined
with context function types as parameters to avoid the plumbing boilerplate
that would otherwise be necessary.
def table(init: Table ?=> Unit) = {
given t as Table
init
t
}
def row(init: Row ?=> Unit)(using t: Table) = {
given r as Row
init
t.add(r)
}
def cell(str: String)(using r: Row) =
r.add(new Cell(str))
With that setup, the table construction code above compiles and expands to:
table { (using $t: Table) =>
row { (using $r: Row) =>
cell("top left")(using $r)
cell("top right")(using $r)
}(using $t)
row { (using $r: Row) =>
cell("bottom left")(using $r)
cell("bottom right")(using $r)
}(using $t)
}
Example: Postconditions
As a larger example, here is a way to define constructs for checking arbitrary postconditions using an extension method ensuring
so that the checked result can be referred to simply by result
. The example combines opaque aliases, context function types, and extension methods to provide a zero-overhead abstraction.
object PostConditions {
opaque type WrappedResult[T] = T
def result[T](using r: WrappedResult[T]): T = r
def (x: T).ensuring[T](condition: WrappedResult[T] ?=> Boolean): T = {
assert(condition(using x))
x
}
}
import PostConditions.{ensuring, result}
val s = List(1, 2, 3).sum.ensuring(result == 6)
Explanations: We use a context function type WrappedResult[T] ?=> Boolean
as the type of the condition of ensuring
. An argument to ensuring
such as
(result == 6)
will therefore have a given of type WrappedResult[T]
in
scope to pass along to the result
method. WrappedResult
is a fresh type, to make sure
that we do not get unwanted givens in scope (this is good practice in all cases
where context parameters are involved). Since WrappedResult
is an opaque type alias, its
values need not be boxed, and since ensuring
is added as an extension method, its argument
does not need boxing either. Hence, the implementation of ensuring
is as about as efficient
as the best possible code one could write by hand:
{ val result = List(1, 2, 3).sum
assert(result == 6)
result
}
Reference
For more info, see the blog article, (which uses a different syntax that has been superseded).