The mechanism necessary for this without casting is not so heavy in this case ... this is just another example of a functional dependency .
In the future, we rely on the fact that the Ref type is sealed, so that we can simply list the alternatives. The Ref and Reference hierarchies remain unchanged, and we add the Rel relation type to both express the correspondence between the levels between them and make the appropriate choice of the value level,
trait Rel[Ref, T] { def lookup(as: List[A], bs: List[B], cs: List[C])(ref: Ref) : Option[T] } object Rel { implicit val relA = new Rel[ARef, A] { def lookup(as: List[A], bs: List[B], cs: List[C])(ref: ARef) : Option[A] = as.find(_.ref == ref) } implicit val relB = new Rel[BRef, B] { def lookup(as: List[A], bs: List[B], cs: List[C])(ref: BRef) : Option[B] = bs.find(_.ref == ref) } implicit val relC = new Rel[CRef, C] { def lookup(as: List[A], bs: List[B], cs: List[C])(ref: CRef) : Option[C] = cs.find(_.ref == ref) } }
Now we can override Context without matching patterns or casts as follows,
trait Context { // ... other stuff ... protected val aList: List[A] = ??? protected val bList: List[B] = ??? protected val cList: List[C] = ??? def get[R <: Ref, T](ref: R)(implicit rel: Rel[R, T]): Option[T] = rel.lookup(aList, bList, cList)(ref) }
And we can use this new definition, for example,
object Test { def typed[T](t: => T) {} // For pedagogic purposes only val context = new Context {} val aRef = ARef("my A ref") val myA = context.get(aRef) typed[Option[A]](myA) // Optional: verify inferred type of myA val bRef = BRef("my B ref") val myB = context.get(bRef) typed[Option[B]](myB) // Optional: verify inferred type of myB val cRef = CRef("my C ref") val myC = context.get(cRef) typed[Option[C]](myC) // Optional: verify inferred type of myC }
Note that resolving the implicit Rel argument to get computes the type of the corresponding Reference from the type of the Ref argument, so we can avoid using any explicit type of arguments on get sites.