0
votes

I'm getting a type erasure warning in Scala.

The problem is that I need to cache outgoing requests. The requests can wrap different return types though due to the way things are currently set up.

I tried to solve it by adding a type parameter the getOrPut method. However in the match statement, due to type erasure, whatever is wrapped in the Future is not getting checked.

I can quiet the type erasure warning by using @unchecked, but I'm wondering if there's a better way to ensure that the returned type is the desired one.

Simplified example:

class RequestCache() {
  val underlying: scala.collection.mutable.Map[String, Future[Any] =
    scala.collection.mutable.Map()

  def getOrPut[A](
    key: String,
    val: Future[Request[A]]
  ): Future[Request[A]] = {
    underlying.get(key) match {
      case None => { 
        underlying.update(key, val)
        val
      }
      case Some(storedVal: Future[Request[A]]) => storedVal
    }
  }
    
}
2

2 Answers

1
votes

It seems your Map value is going be of type Future[Request[A]]]. Why not make class RequestCache take a type parameter, and the type erasure issue won't be present with this approach:

class RequestCache[A] {
  val underlying: scala.collection.mutable.Map[String, Future[Request[A]]] =
    scala.collection.mutable.Map()

  def getOrPut(key: String, value: Future[Request[A]]): Future[Request[A]] =
    underlying.get(key) match {
      case None =>
        underlying.update(key, value)
        value
      case Some(storedVal: Future[Request[A]]) =>
        storedVal
    }
}
0
votes

This cannot be completely solved because you want to put Future of all possible types, so once you forget the exact type once, there is never 100% compile-time guarantee that you can recover it. You would have to cast the type yourself and hope that it matches.

I see 2 ways to solve this problem:

  • encode type in the key (and don't forget it in the key!), so that you would know to which type you can safely cast
  • take the result with erased type and attempt to cast it and handle possible failure

The first could be done e.g. like this:

trait CacheKey {
  type Out
}
object CacheKey {
  type Aux[O] = CacheKey { type Out = O }
}

case object GlobalSetting extends CacheKey { type Out = String }
case class UserSetting(userID: UUID) extends CacheKey { type Out = Int }

class Cache() {
  private val underlying =
    scala.collection.mutable.Map.empty[CacheKey, Future[_]]

  def getOrPut[O](
    key: CacheKey.Aux[O],
    value: => Future[O] // should be by-name!
  ): Future[O] = underlying.get(key) match {
    case Some(storedVal) =>
      storedVal.asInstanceOf[Future[O]]
    case None =>
      underlying.update(key, val)
      value
  }
}

With String you could try to do sth like:

cache.getOrPut("foo", intFuture)
cache.getOrPut("foo", stringFuture)

and the first call would set the cache and the second one... would return a Future with invalid result type, so if you just cast, you would receive ClassCastException at some random point in the Future.

Another way would be to obtain result always and then check if it is valid (which is hard to make bullet proof):

class Cache() {
  private val underlying =
    scala.collection.mutable.Map.empty[CacheKey, (ClassTag[_], Future[_])]

  def getOrPut[O: ClassTag](
    key: String,
    value: => Future[O] // should be by-name!
  ): Future[O] = underlying.get(key) match {
    case Some((originalTag, storedVal)) =>
      if (classTag[O] == originalTag)
        storedVal.asInstanceOf[Future[O]]
      else
        throw new Exception("Mismatched cache types")
    case None =>
      underlying.update(key, val)
      value
  }
}

Problem with that approach is that ClassTag doesn't distinct generics with different type parameters, so e.g. classTag[List[Int]] == classTag[List[String]] returns true. However, with any solution based on runtime reflection you will have this or similar problem, no matter what you pick.

With compile time reflection, you avoid the problem completely, but at all point you have to be aware of the type of the key, so you cannot just pass CacheKey around - because then you will only knew that result is of Future[key.Out] type - you would have to pass CacheKey.Aux[O] around with O as a type parameter. Depending in case that might be a non-issue or a deal breaker.

BTW, you should use by-name paremeters for the Future here - Future starts evaluating the moment you create it, so with your syntax Scala would start evaluating your Future and then it would check in cache if there is some other Future under the same key. If it started and finished earlier it might be some benefits, but you might still end up with a broken logic because that new Future will keep on being executed.