tksfz
tksfz

Reputation: 2970

In Shapeless, given two records, how do I require that both records have the same keys and join them?

Suppose I have two records. One might be the LabelledGeneric representation of a case class; while the other could be a programmer-supplied record that supplies human-readable field labels:

case class Book(author: String, title: String, quantity: Int)
val labels = ('author ->> "Author") :: ('title ->> "Title") :: ('quantity ->> "Quantity") :: HNil

Is there a way to

  1. require that the labelled generic representation of Book and the record type of labels possess the same keys (or at least the keys of label are a subset of the keys of Book) and
  2. "join" or zip them together by key such that you get out a record with the same keys as the left-hand argument, with values being a pair (lhs value, Option[rhs value]) or something like that?

I think this might be doable with a combination of extracting the Keys witness for each side, then using Align. (I'd love to see this added to the out-of-the-box shapeless ops.) This allows us to associate "metadata" to the fields of a class (in lieu of using annotations for example).

Upvotes: 2

Views: 430

Answers (1)

tksfz
tksfz

Reputation: 2970

I think this works but I'd love to hear comments:

trait ZipByKey[L <: HList, R <: HList] extends DepFn2[L, R] {
  type Out <: HList
}

object ZipByKey {

  type Aux[L <: HList, R <: HList, O <: HList] = ZipByKey[L, R] { type Out = O }

  implicit def hnilZip[R <: HList] = new ZipByKey[HNil, R] { type Out = HNil; override def apply(l: HNil, r: R) = HNil }

  implicit def hlistZip[K, V, T <: HList, R <: HList, RV, Remainder <: HList, TO <: HList]
  (implicit
    remover: Remover.Aux[R, K, (RV, Remainder)],
    recurse: ZipByKey.Aux[T, Remainder, TO]
  ) = new ZipByKey[FieldType[K, V] :: T, R] {
    type Out = FieldType[K, (V, RV)] :: TO

    def apply(l: FieldType[K, V] :: T, r: R): Out = {
      val (rv, remainder) = remover.apply(r)
      val newValue = (l.head, rv)
      labelled.field[K](newValue) :: recurse.apply(l.tail, remainder)
    }
  }
}

Example usage:

  case class Book(author: String, title: String, quantity: Int)
  val labels = ('author ->> "Author") :: ('title ->> "Title") :: ('quantity ->> "Number Of") :: HNil

  val generic = LabelledGeneric[Book]

  def zipByKey[T, G <: HList, R <: HList, O <: HList](t: T, r: R)
    (implicit generic: LabelledGeneric.Aux[T, G],
      zipByKey: ZipByKey.Aux[G, R, O]): O = {
    zipByKey.apply(generic.to(t), r)
  }

  println(zipByKey(Book("Hello", "Foo", 3), labels))

prints out

(Foo,Id) :: (Bar,Name) :: (3,Number Of) :: HNil

If we want to allow not all keys to appear in labels then there's a bit more work to do. But there might be other ways to handle that.

Upvotes: 2

Related Questions