The wonder of context functions

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:

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:

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.

Natchez' repository