9
votes

I'm trying to learn Shapeless, and I would like to define a monoid which adds together instances of shapeless records. Note that I'm using algebird monoids (not scalaz), but I'm sure they're quite similar. Here's an example of what I'd like to be able to do:

val result = Monoid.sum(
  ('a ->> 1) :: ('b ->> 1) :: HNil,
  ('a ->> 4) :: ('b ->> 3) :: HNil,
  ('a ->> 2) :: ('b ->> 6) :: HNil)
// result should be: ('a ->> 7) :: ('b ->> 10) :: HNil

I figured out how to write monoid instances for HList, as follows:

  implicit val HNilGroup: Group[HNil] = new ConstantGroup[HNil](HNil)
  implicit val HNilMonoid: Monoid[HNil] = HNilGroup
  class HListMonoid[H, T <: HList](implicit hmon: Monoid[H], tmon: Monoid[T]) extends Monoid[::[H, T]] {
    def zero = hmon.zero :: tmon.zero
    def plus(a: ::[H, T], b: ::[H, T]) = 
      hmon.plus(a.head, b.head) :: tmon.plus(a.tail, b.tail)
  }
  implicit def hListMonoid[H, T <: HList](implicit hmon: Monoid[H], tmon: Monoid[T]) = new HListMonoid[H, T]

This allows me to write:

val result = Monoid.sum(
  1 :: 1 :: HNil,
  4 :: 3 :: HNil,
  2 :: 6 :: HNil)
// result is 7 :: 10 :: HNil

Now that I can sum HList instances, the missing piece seems to be defining monoid instances which can sum fields of form ('name ->> 1), which my IDE tells me has the following type: Int with record.KeyTag[Symbol with tag.Tagged[Constant(name).type] { .. }, Int] { .. }. At this point I'm stuck, as I just don't know how to go about doing this.

1

1 Answers

11
votes

You were very close—you just need to add FieldType[K, H] at each inductive step instead of H and use field[K] to type the values you get from the Monoid[H] appropriately:

import com.twitter.algebird._
import shapeless._, labelled._, record._, syntax.singleton._

implicit val hnilGroup: Group[HNil] = new ConstantGroup[HNil](HNil)
implicit val hnilMonoid: Monoid[HNil] = hnilGroup
implicit def hconsMonoid[K, H, T <: HList](implicit
  hm: Monoid[H],
  tm: Monoid[T]
): Monoid[FieldType[K, H] :: T] =
  Monoid.from(field[K](hm.zero) :: tm.zero) {
    case (hx :: tx, hy :: ty) => field[K](hm.plus(hx, hy)) :: tm.plus(tx, ty)
  }

Or you could use Shapeless's TypeClass machinery, which also gives you instances for case classes, etc.:

import com.twitter.algebird._
import shapeless._, ops.hlist._, ops.record._, record._, syntax.singleton._

object MonoidHelper extends ProductTypeClassCompanion[Monoid] {
  object typeClass extends ProductTypeClass[Monoid] {
    def emptyProduct: Monoid[HNil] = Monoid.from[HNil](HNil)((_, _) => HNil)
    def product[H, T <: HList](hm: Monoid[H], tm: Monoid[T]): Monoid[H :: T] =
      Monoid.from(hm.zero :: tm.zero) {
        case (hx :: tx, hy :: ty) => hm.plus(hx, hy) :: tm.plus(tx, ty)
      }

    def project[F, G](m: => Monoid[G], to: F => G, from: G => F): Monoid[F] =
      Monoid.from(from(m.zero))((x, y) => from(m.plus(to(x), to(y))))
  }

  implicit def deriveRecordInstance[
    R <: HList,
    K <: HList,
    H,
    T <: HList
  ](implicit
    vs: Values.Aux[R, H :: T],        
    vm: Lazy[Monoid[H :: T]],
    ks: Keys.Aux[R, K],
    zk: ZipWithKeys.Aux[K, H :: T, R]
  ): Monoid[R] = typeClass.project(vm.value, vs(_), zk(_: H :: T))
}

import MonoidHelper._

I've provided a derivedRecordInstance method here that makes this work on records, but I'm a little surprised that it's necessary—it's possible that you'll get record instances for free in a future version of Shapeless.