scalaimplicittype-parameterbounded-wildcardunbounded-wildcard

Making parameterized ScalaCache generic with runtime configuration


The git repo that contains the issue can be found here https://github.com/mdedetrich/scalacache-example

The problem that I currently have is that I am trying to make my ScalaCache backend agnostic with it being configurable at runtime using typesafe config.

The issue I have is that ScalaCache parameterizes the constructors of the cache, i.e. to construct a Caffeine cache you would do

ScalaCache(CaffeineCache())

where as for a SentinelRedisCache you would do

ScalaCache(SentinelRedisCache("", Set.empty, ""))

In my case, I have created a generic cache wrapper called MyCache as shown below

import scalacache.ScalaCache
import scalacache.serialization.Codec

final case class MyCache[CacheRepr](scalaCache: ScalaCache[CacheRepr])(
  implicit stringCodec: Codec[Int, CacheRepr]) {

  def putInt(value: Int) = scalaCache.cache.put[Int]("my_int", value, None)
}

We need to carry the CacheRepr along because this is how ScalaCache knows how to serialize any type T. CaffeineCache uses a CacheRepr which is InMemoryRepr where as SentinelRedisCache uses a CacheRepr which is Array[Byte].

And this is where the crux of the problem is, I have an Config which just stores which cache is being used, i.e.

import scalacache.Cache
import scalacache.caffeine.CaffeineCache
import scalacache.redis.SentinelRedisCache

final case class ApplicationConfig(cache: Cache[_])

The reason why its a Cache[_] is because at compile time we don't know what cache is being used, ApplicationConfig will be instantiated at runtime with either CaffeineCache/SentinelRedisCache.

And this is where the crux of the problem is, Scala is unable to find an implicit Codec for the wildcard type if we just us applicationConfig.cache as a constructor, i.e. https://github.com/mdedetrich/scalacache-example/blob/master/src/main/scala/Main.scala#L17

If we uncomment the above line, we get

[error] /Users/mdedetrich/github/scalacache-example/src/main/scala/Main.scala:17:37: Could not find any Codecs for type Int and _$1. Please provide one or import scalacache._
[error] Error occurred in an application involving default arguments.
[error]   val myCache3: MyCache[_] = MyCache(ScalaCache(applicationConfig.cache)) // This doesn't

Does anyone know how to solve this problem, essentially I want to specify that in my ApplicationConfig, cache is of type Cache[InMemoryRepr | Array[Byte]] rather than just Cache[_] (so that the Scala compiler knows to look up implicits of either InMemoryRepr or Array[Byte] and for MyCache to be defined something like this

final case class MyCache[CacheRepr <: InMemoryRepr | Array[Byte]](scalaCache: ScalaCache[CacheRepr])

Solution

  • You seem to be asking for the compiler to resolve implicit values based on the run-time selection of the cache type. This is not possible because the compiler is no longer running by the time the application code starts.

    You have to make the type resolution happen at compile time, not run time. So you need to define a trait the represents the abstract interface to the cache and provide a factory function that returns a specific instance based on the setting in ApplicationConfig. It might look something like this (untested):

    sealed trait MyScalaCache {
      def putInt(value: Int)
    }
    
    object MyScalaCache {
      def apply(): MyScalaCache =
        if (ApplicationConfig.useCaffine) {
          MyCache(ScalaCache(CaffeineCache())
        } else {
          MyCache(ScalaCache(SentinelRedisCache("", Set.empty, ""))
        }
    }
    
    final case class MyCache[CacheRepr](scalaCache: ScalaCache[CacheRepr]) extends MyScalaCache (
      implicit stringCodec: Codec[Int, CacheRepr]) {
    
      def putInt(value: Int) = scalaCache.cache.put[Int]("my_int", value, None)
    }
    

    The compiler will resolve the implicit in MyCache at compile time where the two concrete instances are specified in apply.