Implicit Functions in Scala 3

One of the many new things in Scala 3 (still only available as a pre-release compiler codenamed ‘Dotty’) is the ability to define implicit functions – lambda functions with only implicit parameters.

A few neat things are enabled by using implicit functions as parameters or return values, and I wanted to explore this further.

TL;DR: implicit parameters in Scala 2 are eager to bind to their implicit values, whereas implicit functions as parameters or return values in Scala 3, allows us to bind implicit values lazily.

We will as an example build a very simple StateVerifier using implicit functions to do things we couldn’t do in Scala 2.

First, in order to make more sense to people not writing Scala every day, here’s how you read Scala function definitions;

def functionName(paramName: ParamType): ReturnType = expression

Since Scala has type inference, you can often also omit the return type, as it will be inferred from the expression;

def functionName(paramName: ParamType) = expression

You can also give multiple parameter lists, which is useful to enable partial application, or to provide implicit parameters.

def functionName(paramName: ParamType)(implicit other: OtherType) = expression

And you can define anonymous functions, or lambdas, with arrow syntax, letting the return type be inferred.

(paramName: ParamType) => expression

which has the type Function1[ParamType, ReturnType], more commonly written using arrow notation as (ParamType) => ReturnType, as this is symmetric with the lambda literal syntax.

However, in Scala 2 you could not have lambdas with implicit parameters. Scala 3 adds this, calling them “implicit functions” – lambda functions with nothing but implicit parameters.

Such lambdas are defined using the given keyword:

(given paramName: ParamType) => definition

And their type is ImplicitFunction1[ParamType, ReturnType] or in arrow syntax (given ParamType) => ReturnType.

Now, what I find interesting, is what happens if we define a function that has an implicit function parameter. We can define a StateVerifier with an expect function, and some supporting declarations.

(You can try this out in the dotty REPL, dotr)

import java.time._

case class Person(name: String, age: Int)

// calculate the birth year of an implictly given Person
def birthYear(given p: Person) = LocalDate.now.getYear - p.age

class StateVerifier(currentState: Person) {
    // provides an unnamed implicit value of Person type
    // (in Scala 2, implicts were always named)
    given Person = currentState

    def expect(condition: (given Person) => Boolean): Boolean = {
        // consumes an implicit value of Person type            
        val result = condition

        // could also have been written
        // val result = condition(given currentState)
        // in which case we didn't need an implicit Person

        assert(result, "Expectation failed")
        result
    }
}

And we can define two different state verifiers, one for Arthur, and one for Ford.

val arthurSV = StateVerifier(Person("Arthur", 30))
val fordSV = StateVerifier(Person("Ford", 200))

Note that in Scala 3, we no longer need to use “new” when creating a StateVerifier, something that was only possible for case classes in Scala 2.

Now, this enables us to write;

arthurSV.expect(birthYear > 2000)

The closest we can get to with a Scala 2 version, would have been to modify expect to take a normal lambda, and then utilize lambda shorthand:

arthurSV.expect(birthYear(_) > 2000)

The interesting part is that in Scala 3 the expression birthYear > 2000 is not, as one might assume, evaluated before calling the expect function. This is because we declared the parameter as an implicit function.

It is instead evaluated against the given Person in the StateVerifier, which will result in an AssertionError, since Arthur wasn’t born after the year 2000.

If we write

fordSV.expect(birthYear > 2000)

the expression will be evaluated against the Ford person stored in that instance of StateVerifier, and will in this case also throw an AssertionError.

To understand this behaviour, we can turn to the spec, which says

Implicit function literals (given x1: T1, …, xn: Tn) => e are automatically created for any expression e whose expected type is ImplicitFunctionN[T1, …, Tn, R], unless e is itself a implicit function literal.

Let’s call this the Rewrite Rule, which is useful to keep in mind, in order to understand the behavior of implicit functions.

Note: I can also add that (at least in Dotty 0.19) this means you cannot return an implicit function literal from a larger block, but I’ll have to write more on that later.

As per the Rewrite Rule, the compiler knows that the expected type of the parameter to expect is an implicit function, and adds (given p: Person) => before the expression. Therefore it is compiled as if we had written:

arthurSV.expect( (given p: Person) => birthYear(given p) > 2000 )

This means it does not matter which, if any, implicit Person is in scope at the call to arthurSV.expect, it is the scope in which the implicit function is called that matters, which happens inside expect. When we evaluate the expression

val result = condition

Then condition is our expression birthYear > 2000, which has been converted into an implicit function taking an implicit Person parameter. And since the StateVerifier has set up an implicit Person which is in scope inside the expect function, this compiles.

We could also have made StateVerifier skip setting up an implicit Person, and instead passed currentState (a Person) explicitly, as in

 val result = condition(given currentState)

In Scala 3, we need to again used the given keyword when we are providing an implicit parameter explicitly. This is an improvement in clarity from Scala 2.

Now, further imagine that we are not happy that our StateVerifier throws exceptions, and you want to be able to switch the desired behavior. Perhaps we want to log failed expectations instead, and continue.

There are many ways to modify this aspect of StateVerifier, such as subclassing, or dependency injection, but we will examine returning implicit functions to acheive the same thing.

Let’s add a few type definitions to, I hope, clarify the type signature.

type ExpectationReaction = (Boolean, String) => Unit
type WithReaction = (given ExpectationReaction) => Boolean

WithReaction defines an implicit function type that assumes an implicit ExpectationReaction is available and returns a boolean. The ExpectationReaction is defined as a function type taking a boolean, and a string, returning nothing (Unit). It could just as well have been defined as a trait or a class.

Then let us make StateVerifier generic, and have expect return WithReaction.

class StateVerifier[S](currentState: S) {
    def expect(condition: (given S) => Boolean, msg: String): WithReaction = {
        // fetch the reactor from implicit scope
        val reactor = summon[ExpectationReaction]
        val result = condition(given currentState)
        // execute the reactor with the result and a message
        reactor(result, msg)

        // return the result
        result
    }
}

The full type signature for expect is obtained by expanding the type declarations step by step;

def expect(...): WithReaction = { ... }
def expect(...): given ExpectationReaction => Boolean = { ... }
def expect(...): given ((Boolean, String) => Unit) => Boolean = { ... }

These are all equivalent, and as per the Rewrite Rule compiled as if we’d written

def expect(...) = { given (reactor: (Boolean, String) => Unit) => ... }

meaning that the body of the method can access the implicit reactor parameter that will be provided when executing the implicit function. There’s a problem, though, since we don’t have a name to access the parameter by. The solution is to summon the parameter.

The method summon (called implicitly in Scala 2) returns the given (implicit) instance that’s in scope, for a specific type, or throws an exception if none found.

An example REPL session (where scala> is the REPL prompt) using these definitions could be:

var zaphodSV = StateVerifier(Person("Zaphod", 42))

// throw AssertionError on failed expectation, as before
given ExpectationReaction = (v, msg) => assert(v, msg)

scala> zaphodSV.expect(birthYear > 2000, "millennials only")
java.lang.AssertionError: assertion failed: millennials only

We could also decide to simply print a message on failed expectation instead of throwing an exception, by providing a different reaction.

def printReaction(v: Boolean, msg: String): Unit = if(!v) {
    println("Expectation failed: " + msg)
}    

given ExpectationReaction = printReaction

scala> val res = zaphodSV.expect(birthYear > 2000, "millennials only")
Expectation failed: millennials only
val res: Boolean = false

In fairness, this could have been achieved by Scala 2 implicits (if expect took an implicit parameter instead of returning an implicit function), so here’s an example of something that cannot be done in Scala 2;

class ReactionLogger {
    def logReaction(v: Boolean, msg: String): Unit = {
        val outcome = if(v) { "success" } else { "failure" }
        println(s"Logging $outcome: $msg")
    }    

    def evaluate(reactor: WithReaction): Boolean = {
        reactor(given logReaction)
    }
}

val logger = ReactionLogger()
scala> val res = logger.evaluate(zaphodSV.expect(birthYear > 2000, "millennials only"))
Logging failure: millennials only
val res: Boolean = false

Using Scala 2 implicits, the line

logger.evaluate(zaphodSV.expect(birthYear > 2000, "millennials only"))

would not have been able to “wait” for the ExpectationReaction to be provided inside logger.evaluate, it would have needed (and consumed) one in scope before logger.evaluate was called, which would have defeated the purpose of the ReactionLogger.

In summary, the difference between:

def b1 (given p: Person): Int = ...
def b2: (given Person) => Int = ...
val b3 = (given p: Person) => ...

is that b1 defines a function that takes an implicit Person and returns an Int. b2 defines a parameterless function that returns an implicit function taking a Person and returns an Int. b3 defines a value as an implicit function taking a Person and returning an Int

b2 and b3 has the same type, the implicit function type (given Person) => Int which wasn’t present in Scala 2.

b1 captures an implicit Person in scope at the call site, whereas the implicit function b3 and the return value of b2 can be stored and passed around as implicit function values, and executed elsewhere to capture a Person in scope at that location.

In short, implicit parameters bind eagerly to implicit values, whereas implicit functions allows us to bind lazily.

Leave a Reply

Close Menu