Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to return correct type from generic function passed a related abstract type parameter

Tags:

scala

I am trying to write "better" (more idiomatic?) Scala code for the following circumstance: I have a set of classes that will be identified by a reference field that belongs to a parallel set of reference case classes, something like the following:

abstract sealed class Ref(value: String)

case class ARef(value: String) extends Ref(value)
case class BRef(value: String) extends Ref(value)
case class CRef(value: String) extends Ref(value)

trait Referenced {
  type refType <: Ref
  val ref: refType
}

trait A extends Referenced { type refType = ARef }
trait B extends Referenced { type refType = BRef }
trait C extends Referenced { type refType = CRef }

Another class (which will probably turn into the state type of a State monad) will contain lists of these types, and provide a function to retrieve an object, given its reference. I want this returned value to be appropriately typed, i.e. given

val aRef = ARef("my A ref")

I want to be able to make a call like:

val myA: Option[A] = context.get[A](aRef)

and be sure to get back an Option[A], not just an Option[Referenced]. My best attempt to achieve this so far looks something like the following:

trait Context {

  // ... other stuff ...

  protected val aList: List[A]
  protected val bList: List[B]
  protected val cList: List[C]

  def get[R <: Referenced](ref: R#refType): Option[R] = {
    val result = ref match {
      case aRef: ARef => aList.find(_.ref == aRef)
      case bRef: BRef => bList.find(_.ref == bRef)
      case cRef: CRef => cList.find(_.ref == cRef)
      case _ => throw new RuntimeException("Unknown Ref type for retrieval: "+ref)
    }
    result.asInstanceOf[Option[R]]
  }
}

which seems to work correctly, but has that smelly "asInstanceOf" call in it. I would be interested in seeing ideas on how this might be done better (and check that I haven't just missed an obvious simpler solution).

Note that for other reasons, I have thus far elected to go with abstract typing rather than parameter types (trait A extends Referenced[ARef] style), but could change this if the reasons were compelling enough.

like image 757
Shadowlands Avatar asked Jan 12 '23 23:01

Shadowlands


1 Answers

The machinery needed to do this without casting really isn't all that heavy in this case ... it's just another example of a functional dependency.

In what follows we rely on the fact that type Ref is sealed so that we can simply enumerate the alternatives. Your Ref and Reference hierarchies remain unchanged, and we add a relation type Rel to both express the type-level correspondence between the two, and to make an appropriate value-level selection,

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 reimplement Context without pattern matches 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 like so,

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
}

Notice that the resolution of the implicit Rel argument to get computes the type of the corresponding Reference from the type of the ref argument, so we're able to avoid having to use any explicit type arguments at get's call-sites.

like image 132
Miles Sabin Avatar answered Feb 01 '23 10:02

Miles Sabin