There are a number of ways you could do this, but I'd go with a custom type class:
import shapeless._
trait RowSelect[L <: HList] extends DepFn2[L, Int] {
type Row <: HList
type Out = Option[Row]
}
object RowSelect {
def select[L <: HList](l: L, i: Int)(implicit rs: RowSelect[L]): rs.Out = rs(l, i)
type Aux[L <: HList, Row0 <: HList] = RowSelect[L] { type Row = Row0 }
implicit val hnilRowSelect: Aux[HNil, HNil] = new RowSelect[HNil] {
type Row = HNil
def apply(l: HNil, i: Int): Option[HNil] = Some(HNil)
}
implicit def hconsRowSelect[A, T <: HList](implicit
trs: RowSelect[T]
): Aux[List[A] :: T, A :: trs.Row] = new RowSelect[List[A] :: T] {
type Row = A :: trs.Row
def apply(l: List[A] :: T, i: Int): Option[A :: trs.Row] = for {
h <- l.head.lift(i)
t <- trs(l.tail, i)
} yield h :: t
}
}
Which works like this:
scala> println(RowSelect.select(a, 0))
Some(1 :: a :: true :: HNil)
scala> println(RowSelect.select(a, 1))
Some(2 :: b :: false :: HNil)
scala> println(RowSelect.select(a, 2))
Some(3 :: c :: true :: HNil)
scala> println(RowSelect.select(a, 3))
None
A RowSelect
instance for L
witnesses that L
is an hlist with all List
elements, and provides an operation that optionally selects the item at a specified index from each List
.
You should be able to accomplish the same thing with NatTRel
or a combination of ConstMapper
and ZipWith
and a Poly2
, but a custom type class bundles everything together nicely and in many cases allows more convenient compositionality.
For example, in this case the solution to your bonus question can be pretty straightforwardly written in terms of RowSelect
:
def allRows[L <: HList](l: L)(implicit rs: RowSelect[L]): List[rs.Row] =
Stream.from(0).map(rs(l, _).toList).takeWhile(_.nonEmpty).flatten.toList
And then:
scala> allRows(a).foreach(println)
1 :: a :: true :: HNil
2 :: b :: false :: HNil
3 :: c :: true :: HNil
And similarly if you want to convert these hlists to tuples:
def allRows[L <: HList, R <: HList](l: L)(implicit
rs: RowSelect.Aux[L, R],
tp: ops.hlist.Tupler[R]
): List[tp.Out] =
Stream.from(0).map(rs(l, _).map(tp(_)).toList).takeWhile(_.nonEmpty).flatten.toList
Which gives you:
scala> allRows(a)
res7: List[(Int, String, Boolean)] = List((1,a,true), (2,b,false), (3,c,true))
And so on.