The wonder of context functions

markehammons

Mark Hammons

Posted on December 21, 2021

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"))
Enter fullscreen mode Exit fullscreen mode

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 become A ?=> B ?=> C automatically
  • Can uncurry: A ?=> B ?=> C is equivalent to (A,B) ?=> C
  • Don't care about order: A ?=> B ?=> C is equivalent to B ?=> 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

These dependencies can be composed

val value1: Barred[Fooed[String]] = 
  (method(3)*fn(2.0)).toString
Enter fullscreen mode Exit fullscreen mode

Reordered

val value2: Fooed[Barred[String]] = value1
Enter fullscreen mode Exit fullscreen mode

And eliminated piece by piece:

val value3: Fooed[String] = 
  given Barrer with
    def bar(d: Double) = d.toString
  value2
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
markehammons
Mark Hammons

Posted on December 21, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Make Invalid States Unrepresentable
The wonder of context functions
scala The wonder of context functions

December 21, 2021