Memoization of method results

scala> import scalacache._
import scalacache._

scala> import scalacache.memcached._
import scalacache.memcached._

scala> import scalacache.memoization._
import scalacache.memoization._

scala> import scalacache.serialization.binary._
import scalacache.serialization.binary._

scala> import scalacache.modes.try_._
import scalacache.modes.try_._

scala> import scala.concurrent.duration._
import scala.concurrent.duration._

scala> import scala.util.Try
import scala.util.Try

scala> final case class Cat(id: Int, name: String, colour: String)
defined class Cat

scala> implicit val catsCache: Cache[Cat] = MemcachedCache("localhost:11211")
catsCache: scalacache.Cache[Cat] = scalacache.memcached.MemcachedCache@6a9e8a0c

scala> // You wouldn't normally need to specify the type params for memoize.
     | // This is an artifact of the way this README is generated using tut.
     | def getCat(id: Int): Try[Cat] = memoize[Try, Cat](Some(10.seconds)) {
     |   // Retrieve data from a remote API here ...
     |   Cat(id, s"cat ${id}", "black")
     | }
getCat: (id: Int)scala.util.Try[Cat]

scala> getCat(123)
res2: scala.util.Try[Cat] = Success(Cat(123,cat 123,black))

Did you spot the magic word ‘memoize’ in the getCat method? Just adding this keyword will cause the result of the method to be memoized to a cache. The next time you call the method with the same arguments the result will be retrieved from the cache and returned immediately.

If the result of your block is wrapped in an effect container, use memoizeF:

scala> def getCatF(id: Int): Try[Cat] = memoizeF[Try, Cat](Some(10.seconds)) {
     |   Try {
     |     // Retrieve data from a remote API here ...
     |     Cat(id, s"cat ${id}", "black")
     |   }
     | }
getCatF: (id: Int)scala.util.Try[Cat]

scala> getCatF(123)
res3: scala.util.Try[Cat] = Success(Cat(123,cat 123,black))

Synchronous memoization API

Again, there is a synchronous memoization method for convient use of the synchronous mode:

scala> import scalacache.modes.sync._
import scalacache.modes.sync._

scala> def getCatSync(id: Int): Cat = memoizeSync(Some(10.seconds)) {
     |   // Do DB lookup here ...
     |   Cat(id, s"cat ${id}", "black")
     | }
getCatSync: (id: Int)Cat

scala> getCatSync(123)
res4: Cat = Cat(123,cat 123,black)

How it works

memoize automatically builds a cache key based on the method being called, and the values of the arguments being passed to that method.

Under the hood it makes use of Scala macros, so most of the information needed to build the cache key is gathered at compile time. No reflection or AOP magic is required at runtime.

Cache key generation

The cache key is built automatically from the class name, the name of the enclosing method, and the values of all of the method’s parameters.

For example, given the following method:

package foo

object Bar {
  def baz(a: Int, b: String)(c: String): Int = memoizeSync(None) {
    // Reticulating splines...   
    123
  }
}

the result of the method call

val result = Bar.baz(1, "hello")("world")

would be cached with the key: foo.bar.Baz(1, hello)(world).

Note that the cache key generation logic is customizable. Just provide your own implementation of MethodCallToStringConverter

Enclosing class’s constructor arguments

If your memoized method is inside a class, rather than an object, then the method’s result might depend on values passed to that class’s constructor.

For example, if your code looks like this:

package foo

class Bar(a: Int) {

  def baz(b: Int): Int = memoizeSync(None) {
    a + b
  }
  
}

then you want the cache key to depend on the values of both a and b. In that case, you need to use a different implementation of MethodCallToStringConverter, like this:

implicit val cacheConfig = CacheConfig(
  memoization = MemoizationConfig(MethodCallToStringConverter.includeClassConstructorParams)
)

Doing this will ensure that both the constructor arguments and the method arguments are included in the cache key:

new Bar(10).baz(42) // cached as "foo.Bar(10).baz(42)" -> 52
new Bar(20).baz(42) // cached as "foo.Bar(20).baz(42)" -> 62

Excluding parameters from the generated cache key

If there are any parameters (either method arguments or class constructor arguments) that you don’t want to include in the auto-generated cache key for memoization, you can exclude them using the @cacheKeyExclude annotation.

For example:

def doSomething(userId: UserId)(implicit @cacheKeyExclude db: DBConnection): String = memoize {
  ...
}

will only include the userId argument’s value in its cache keys.