The wonder of context functions
Mark Hammons
Posted on December 21, 2021
There have been many new features released in Scala 3, but one of the ones that had seemingly little use to me was context functions.
Context functions in Scala 3 are the ability to express a function that needs context (not direct input) in order to evaluate. This probably doesn't mean a whole lot in black and white, so I'll demonstrate with an example:
//a method that needs two Int inputs
def add(a: Int, b: Int) = a + b
//a function that needs two Int inputs
val add = (a: Int, b: Int) = a + b
//a method in scala 2 style that requires context
def getExecutionContext(implicit ec: ExecutionContext): Unit = ec.reportFailure(new Exception("foo"))
//a function that requires context
val getExecutionContext = (ec: ExecutionContext) ?=> ec.reportFailure(new Exception("foo"))
Basically, context functions are to methods that require context what functions are to methods. You can capture and pass around a context function, and even assign it to a val. This does not seem to be much to write home about, but it's actually a very powerful concept.
Dependency Injection
Dependency injection is something that's frequently needed in Scala, and the community has produced a number of solutions for it. Some examples are
- Macwire - Stitches together classes based on their constructors to provide compile time dependency injection
- Reader monad and Kleisli - The reader monad is a category theory compatible way to build up and inject dependencies.
- ZIO's RIO and URIO types - These effect types carry around context with them and can be used for dependency injection purposes
- Implicits - these have been suggested as a method of dependency injection
With context functions, implicits become incredibly suitable for dependency injection, and may be the best option for the pattern. This is because the parameters of context functions:
- Can curry:
(A,B) ?=> C
can becomeA ?=> B ?=> C
automatically - Can uncurry:
A ?=> B ?=> C
is equivalent to(A,B) ?=> C
- Don't care about order:
A ?=> B ?=> C
is equivalent toB ?=> A ?=> C
- Can be inferred:
val getExecutionContext: ExecutionContext ?=> Unit = summon[ExecutionContext].report(new Exception("foo"))
is equivalent to our previous implementation.
These four properties add up to a powerful dependency injection mechanism:
trait Fooer:
def foo(a: Int): Int
type Fooed[A] = Fooer ?=> A
val fooer: Fooed[Fooer] = summon[Fooer]
def fn(i: Int): Fooed[String] =
fooer.foo(i)
val value: Fooed[Double] = fn(3).toDouble
@main
def run =
given Fooer with
def foo(a: Int) = a * 3
println(value)
As you can see above, fn
needed a Fooer
, which needed to be passed into value
if value
wanted to use fn
. In Scala 2, this would've necessitated that value
be a method, but now it can be a val. But what about mixing dependencies?
trait Fooer:
def foo(a: Int): Int
type Fooed[A] = Fooer ?=> A
val fooer: Fooed[Fooer] = summon[Fooer]
trait Barrer:
def bar(d: Double): String
type Barred[A] = Barrer ?=> A
val barrer: Barred[Barrer] = summon[Barrer]
def method(i: Int): Fooed[String] =
fooer.foo(i).toString
def fn(d: Double): Barred[Short] =
barrer.bar(d).toShort
These dependencies can be composed
val value1: Barred[Fooed[String]] =
(method(3)*fn(2.0)).toString
Reordered
val value2: Fooed[Barred[String]] = value1
And eliminated piece by piece:
val value3: Fooed[String] =
given Barrer with
def bar(d: Double) = d.toString
value2
As you can see, context functions make for a powerful dependency injection mechanism. They also don't require mapping, flatmapping, etc like the Reader monad requires. Best of all, this abstraction doesn't have the runtime overhead of the Reader monad or Kleisli, and so can be used to add context to any effect type without paying a price for it.
Regarding real-world uses of this concept, I used it today to put natchez tracing in my http4s project. While the project is still small, I was shocked at the lack of invasiveness of this approach compared to usage of Kleisli to achieve the same effect.
While Scala 3 has introduced a massive amount of new, helpful concepts, the introduction of context functions may well be one of the most helpful, and yet most underrated of these.
Posted on December 21, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.