When I recently looked at implicit functions in Scala 3 (still only available as a pre-release compiler codenamed ‘Dotty’) I wondered: What if we need to perform an expensive operation before returning an implicit function literal?
This turned out to be a lot harder to achieve than I anticipated. Lets first look at how this can be done via an ordinary lambda function, though, before going any further.
(You can try this out in the dotty REPL, dotr)
case class Person(name: String, age: Int)
def calculateAverageAge(): Int = {
// assume some expensive calculation needs to be done
println("expensive calculation")
42
}
def olderThanAverage() = {
val average = calculateAverageAge()
(p: Person) => p.age > average
}
scala> val oldieCheck = olderThanAverage()
expensive calculation
val oldieCheck: Person => Boolean = Lambda$1530/0x000000080100d840@749ee0e3
scala> val isOld = oldieCheck(Person("Greta", 16))
val isOld: Boolean = false
scala> val isOld = oldieCheck(Person("Ford", 200))
val isOld: Boolean = true
Here, we can see that the expensive calculation is performed only once. Is is performed when declaring oldieCheck
which calls the olderThanAverage
function, which first performs the expensive calculation, and then returns a lambda that uses the calculated value. The oldieCheck can be called several times without needing to perform the expensive calculation again.
Now, we want to do the same thing, but this time using implicit functions!
Our first try might be;
def olderThanAverage1() = {
val average = calculateAverageAge()
(given p: Person) => p.age > average
}
But this does not even compile (you get no implicit argument of type Person was found for parameter of (given Person) => Boolean)
unless you have an implicit Person in scope. This is because olderThanAverage1
is inferred to have a Boolean return type, and compiler is trying to get a Boolean by immediately executing the implicit function!
This means that if you do have an implicit Person in scope, say Person("Ford", 200)
, then olderThanAverage1
compiles, but is forever bound to this Person and will not even return a lambda, but a constant true
value. Not what we wanted.
A second try might be to explicitly state the return type;
def olderThanAverage2(): (given Person) => Boolean = {
val average = calculateAverageAge()
(given p: Person) => p.age > average
}
this also doesn’t compile, because it expects the last statement to have Boolean return type. To understand these behaviors, we need to look at the Rewrite Rule in the Scala 3 spec;
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.
In these cases the compiler sees that the function body block has implicit function type, and is not itself an implicit function literal (it’s a block returning one), so it adds another (given p: Person) =>
before the block to make the entire expression an implicit function literal, expecting the block itself to return a Boolean. Please see the discussion on the Rewrite Rule in my previous post if this is unclear.
We can try to explicitly use the return keyword to return the implicit function, which is normally not needed in Scala code:
def olderThanAverage3(): (given Person) => Boolean = {
val average = calculateAverageAge()
return (given p: Person) => p.age > average
}
However, this in fact crashes the dotr 0.19-RC1 REPL with Exception in thread "main" java.lang.IllegalArgumentException: Could not find proxy for val nonLocalReturnKey1: Object in List(val nonLocalReturnKey1, method olderThanAverage3
!
A fourth try could be to return the lambda indirectly:
def olderThanAverage4(): (given Person) => Boolean = {
val average = calculateAverageAge()
val aboveAverage = (given p: Person) => p.age > average
aboveAverage
}
This compiles, but does not work as intended, since if we then try to use the function, by
val oldieCheck = olderThanAverage4()
it does not compile due to the lack of an implicit Person (i.e. we are trying to eagerly bind to an implicit to produce a Boolean value for oldieCheck, not a lambda). If we rewrite the check with an explicit return type;
val oldieCheck: (given Person) => Boolean = olderThanAverage4()
It compiles, but the expensive operation is now executed on each invocation of oldieCheck. This again is explained by looking at the rewriting done by the compiler as per the Rewrite Rule;
def olderThanAverage4() = (given p1: Person) => {
val average = calculateAverageAge()
val aboveAverage = (given p2: Person) => p.age > average
aboveAverage
}
So, after the rewrite, our expensive calculation is now inside the body of the returned lambda, and will be executed on each invocation of the lambda.
So, we have not found a working solution, apart from the first one using an ordinary lambda. But what if we combine the two approaches, and try to return a lambda that returns the implicit function we want? Let’s try that out:
def olderThanAverage5() = {
val average = calculateAverageAge()
// return a lambda that will return the implicit function
() => (given p: Person) => p.age > average
}
scala> val oldieCheckGenerator = olderThanAverage5()
expensive calculation
val oldieCheckGenerator: () => (given Person) => Boolean = Lambda$1646/0x0000000801166440@2e096ac0
Now, declaring oldieCheckGenerator
executed the expensive calculation, and returned a lambda that can give us the implict function we want.
scala> val oldieCheck: (given Person) => Boolean = oldieCheckGenerator()
val oldieCheck: (given Person) => Boolean = Lambda$1690/0x0000000801189840@28532753
We need to be explicit about the type of oldieCheck, as above, or it’ll be inferred to be a Boolean, not a function type.
Now we can can call oldieCheck, explicitly passing in a Person;
scala> val isOld = oldieCheck(given Person("Greta", 16))
val isOld: Boolean = false
or just execute it if we have an implicit Person in scope;
given Person = Person("Ford", 200)
scala> val isOld = oldieCheck
val isOld: Boolean = true
And neither execution of oldieCheck needed to perform the expensive calculation again.
Please note that if you try to be clever and bypass storing the lambda in the oldieCheckGenerator, as in
// call the expensive function to get the lambda
// and immediately execute the lambda to get the
// implicit function
val badOldieCheck: (given Person) => Boolean = olderThanAverage5()()
This will not work as intended, and again we are tripped up by the Rewrite Rule, as the above will be rewritten into
val badOldieCheck = (given Person) => olderThanAverage5()()
which again moves the expensive part into the implicit function. In fact, we can now realize that the intermediate oldieCheckGenerator lambda is invoked on each call of oldieCheck for the same reason, but as this is not an expensive operation, it has no performance impact.
So, the answer to the original question is; You cannot return an implicit function literal from a block due to the Rewrite Rule. However, you can get around this limitation by returning an ordinary lambda that returns the implicit function.
I guess we’ll have to wait and see if the Scala 3 release will make this easier.