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 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) ?=> Ccan become
A ?=> B ?=> Cautomatically
- Can uncurry:
A ?=> B ?=> Cis equivalent to
(A,B) ?=> C
- Don't care about order:
A ?=> B ?=> Cis 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)
As you can see above,
fn needed a
Fooer, which needed to be passed into
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
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.