If we consider a heterogenous list, wherein the elements of the list have varied but known types, it might be desirable to be able to perform operations on the elements of the list collectively without discarding the elements' type information. The following example implements a mapping operation over a simple heterogenous list.
Because the element type varies, the class of operations we can perform is restricted to some form of type projection, so we define a trait Projection
having abstract type Apply[A]
computing the result type of the projection, and def apply[A](a: A): Apply[A]
computing the result value of the projection.
trait Projection {
type Apply[A] // <: Any
def apply[A](a: A): Apply[A]
}
In implementing type Apply[A]
we are programming at the type level (as opposed to the value level).
Our heterogenous list type defines a map
operation parametrized by the desired projection as well as the projection's type. The result of the map operation is abstract, will vary by implementing class and projection, and must naturally still be an HList
:
sealed trait HList {
type Map[P <: Projection] <: HList
def map[P <: Projection](p: P): Map[P]
}
In the case of HNil
, the empty heterogenous list, the result of any projection will always be itself. Here we declare trait HNil
as a convenience so that we may write HNil
as a type in lieu of HNil.type
:
sealed trait HNil extends HList
case object HNil extends HNil {
type Map[P <: Projection] = HNil
def map[P <: Projection](p: P): Map[P] = HNil
}
HCons
is the non-empty heterogenous list. Here we assert that when applying a map operation, the resulting head type is that which results from the application of the projection to the head value (P#Apply[H]
), and that the resulting tail type is that which results from mapping the projection over the tail (T#Map[P]
), which is known to be an HList
:
case class HCons[H, T <: HList](head: H, tail: T) extends HList {
type Map[P <: Projection] = HCons[P#Apply[H], T#Map[P]]
def map[P <: Projection](p: P): Map[P] = HCons(p.apply(head), tail.map(p))
}
The most obvious such projection is to perform some form of wrapping operation - the following example yields an instance of HCons[Option[String], HCons[Option[Int], HNil]]
:
HCons("1", HCons(2, HNil)).map(new Projection {
type Apply[A] = Option[A]
def apply[A](a: A): Apply[A] = Some(a)
})