First of all, you don't need all the existentials here. In some cases they're just overly verbose—K forSome { type K <: Resolver[_] }
, for example, is exactly the same as K <: Resolver[_]
. In other cases they introduce behavior that seems to be unintended. For example:
class FooResult extends Result { def computed = true }
class BarResult extends Result { def computed = true }
object Foo extends Resolver[FooResult] { def result() = new BarResult }
This compiles just fine, because the T
in the return type of result
shadows the class's type parameter T
.
Next, have you considered lazy values here? They'd take care of this kind of caching for you—you just define result
as lazy val result = ...
in your Resolver
implementations and the result will be computed exactly once (when it's first needed).
If you decide you need to handle this caching manually, this is a situation where you may be able to justify bullying the compiler with a cast. For example, given the following setup:
trait Result { def computed: Boolean }
trait Resolver[T <: Result] { def result(): T }
We can write:
object Loader {
private[this] val cache = collection.mutable.Map.empty[Resolver[_], Result]
def get[V <: Result](k: Resolver[V]): V =
cache.getOrElseUpdate(k, k.result()).asInstanceOf[V]
}
We know that the types will work out, since there's only one way for pairs to get into the map.
Update: My Loader
solution here is essentially the same as Sergey's (note though that it's a good idea to lock down the map, and that you don't need brackets around the call to result
, since the second parameter of getOrElseUpdate
is by-name). I'm leaving my answer anyway because the first half is the important part.