Pick out the Nth element of a HList of Lists and r

2020-06-16 03:57发布

问题:

I have an HList in which each column represents a column of a table. Each list in the HList is of the same length.

I'd like to be able to write a function which picks out individual rows of this table as a tuple or an HList of values. Eventually I will convert this to something a bit more sensible (e.g. a Case Class).

import shapeless.PolyDefns.~>
import shapeless.{HList, HNil}
val a = List(1,2,3) :: List("a", "b", "c") :: List(true, false, true) :: HNil

object broken extends (HList ~> HList) {
  def apply[T](n:Int, l:HList): HList = {
    // I want to pick out the nth element of each HList
    // so in the above example, if n==1
    // I want to return
    // 2 :: "b" :: false :: HNil
    ???
  }
}

broken(1,a)

Can I fix this function so that it works according to what I've described in the comments?

Bonus points: Can I write this as an iterator that transforms my HList "a" above into a sequence of (Int, String, Boolean) or an equivalent HList?

回答1:

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.