Scrap Your Boilerplate with Scala 3 Context Functions :: Feb 19, 2022

We program with contexts all the time. Some examples are Http request time, database transactions, or the ability to suspend a Coroutine.

It quickly gets tedious passing contexts method to method. That’s why Spring Boot has annotations like @Transactional and @Async. But Spring’s annotations aren’t typesafe, and they don’t compose well either. If you write a @Transactional method, and somewhere deep in the callstack an @Async method gets called.. your database state can get corrupted. Fun.

Scala’s alternative is implicit parameters.

def doSomething(user: User)(using Transaction, Async, Time): UserResp = ???

It’s a lot easier then passing the contexts manually, but still annoying having to update method signatures when things change.

Assuming these three contexts are the most common, you can scrap the boilerplate by defining a Scala 3 Context Function type:

type IO[A] = (Transaction, Async, Time) ?=> A

def doSomething(user: User): IO[UserResp] = ???

Although equivalent to first example, I thought it would compile to something like this:

def doSomething(user: User): Function3[Transaction, Async, Time, UserResp] = ???

But CRF reveals the Scala 3 compiler is quite smart and avoids returning a function:

def doSomething(user: User, ev$1: Transaction, ev$2: Async, ev$3: Time): UserResp = ???

Aside: Is it right to call this Context Function type ‘IO’?