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.
A HList is a List where the type of every element is statically known at compile time. You may see them as "tuples on steroid". The beauty of HList compared to tuples is that you'll find all the essential List methods like take , head , tail , map , flatMap , zip , etc.
shapeless is a type class and dependent type based generic programming library for Scala.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With