You can leverage the fact that Scala is an OOP and FP language at the same time and that functions in Scala are objects.
object CachedFunction extends App {
val f = new Function2[Int, Int, Int] {
def expensiveCalculation(num: Int) = {
println("I've spent a lot of time(!) calculating square of " + num)
num * num
}
var precomputed: Map[Int, Int] = Map()
def getOrUpdate(key: Int): Int =
precomputed.get(key) match {
case Some(v) => v
case None =>
val newV = expensiveCalculation(key)
precomputed += key -> newV
newV
}
def apply(x: Int, y: Int): Int =
getOrUpdate(x) + getOrUpdate(y)
}
def g = f(1, _: Int)
g(2)
g(3)
g(3)
f(1, 2)
}
prints:
I've spent a lot of time(!) calculating square of 1
I've spent a lot of time(!) calculating square of 2
I've spent a lot of time(!) calculating square of 3
I've changed f
from def
to val
- that allows f to be an object that "stores" the function rather than just being a method that runs its whole body every time. In this case only apply
is run every time and instance variables of the function object are preserved. The rest is OOPish kind of way.
Although this can be considered immutable for the caller because the returned result does not change over time it's not thread safe.You might want to use some sort of synchronized map for storing cached values.
EDIT:
After I wrote this I googled for "function memoization" and got these similar solutions. They are more generic though:
Scala Memoization: How does this Scala memo work?
Is there a generic way to memoize in Scala?
http://eed3si9n.com/learning-scalaz-day16
Apparently there is even something in Scalaz :)
EDIT:
The problem is that Scala does not eagerly evaluate arguments of the function even if the function is partially applied or curried. It simply stores the values of the arguments. Here is an example:
object CachedArg extends App {
def expensiveCalculation(num: Int) = {
println("I've spent a lot of time(!) calculating square of " + num)
num * num
}
val ff: Int => Int => Int = a => b => expensiveCalculation(a) + expensiveCalculation(b)
val f1 = ff(1) // prints nothing
val e1 = expensiveCalculation(1) // prints for 1
val f: (Int, Int) => Int = _ + expensiveCalculation(_)
val g1 = f(e1, _: Int)
g1(2) // does not recalculate for 1 obviously
g1(3)
}
Prints:
I've spent a lot of time(!) calculating square of 1
I've spent a lot of time(!) calculating square of 2
I've spent a lot of time(!) calculating square of 3
This shows that you can still evaluate an argument once manually and "save" it by partially applying it to the function (or currying). I think that's what you were going after. To have a more convenient way you can use this approach:
object CachedFunction extends App {
val f = new Function1[Int, Int => Int] {
def expensiveCalculation(num: Int) = {
println("I've spent a lot of time(!) calculating square of " + num)
num * num
}
def apply(x: Int) =
new Function[Int, Int] {
val xe = expensiveCalculation(x)
def apply(y: Int) = xe + expensiveCalculation(y)
}
}
val g1 = f(1) // prints here for eval of 1
g1(2)
g1(3)
}
Prints:
I've spent a lot of time(!) calculating square of 1
I've spent a lot of time(!) calculating square of 2
I've spent a lot of time(!) calculating square of 3
However, in both last examples, memoizaition is local to the function object. You have to reuse the same function object for it to work. Unlike that, in the first example memoizaition is global to the scope where the function is defined.