I'm reading a book with the following:
sealed trait Currency
case object USD extends Currency
... other currency types
case class Money(m: Map[Currency, BigDecimal]) {
... methods defined
}
The discussion goes on to recognize certain types of operations on Money
as being Monoidal so we want to create a Monoid
for Money
. What comes next though are listings I can't parse properly.
First is the definition of zeroMoney
. This is done as follows:
final val zeroMoney: Money = Money(Monoid[Map[Currency, BigDecimal]].zero)
What I have trouble following here is the part inside the Money
parameter list. Specifically the
Monoid[Map[Currency, BigDecimal]].zero
Is this supposed to construct something? So far in the discussion there hasn't been an implementation of the zero
function for Monoid[Map[A,B]]
so what does this mean?
Following this is the following:
implicit def MoneyAdditionMonoid = new Monoid[Money] {
val m = implicitly(Monoid[Map[Currency, BigDecimal]])
def zero = zeroMoney
def op(m1: Money, m2: Money) = Money(m.op(m1.m, m2.m))
}
The definition of op
is fine given everything else so that isn't a problem. But I still don't understand what zeroMoney
is given its definition. This also gives me the same problem with the implicit m
as well.
So, just what does Monoid[Map[Currency, BigDecimal]]
actually do? I don't see how it constructs anything since Monoid
is a trait with no implementation. How can it be used without defining op
and zero
first?
For this code to compile, you would need something like the following:
trait Monoid[T] {
def zero: T
def op(x: T, y: T): T
}
object Monoid {
def apply[T](implicit i: Monoid[T]): Monoid[T] = i
}
So Monoid[Map[Currency, BigDecimal]].zero
desugars into Monoid.apply[Map[Currency, BigDecimal]].zero
, which simplifies to implicitly[Monoid[Map[Currency, BigDecimal]]].zero
.
zero
in the Monoidal context is the element such that
Monoid[T].op(Monoid[T].zero, x) ==
Monoid[T].op(x, Monoid[T].zero) ==
x
In the case of Map
, I would assume the Monoid
combines Maps with ++
. The zero
would then simply be Map.empty
, which is what Monoid[Map[Currency, BigDecimal]].zero
finally simplifies into.
Edit: answer to comment:
Note that implicit conversion is not used at all here. This is the type class pattern which uses only implicit parameters.
Map[A, B]
is aMonoid
ifB
is aMonoid
That's one way to do it, which is different from the one I suggested with ++
. Let's see an example. How would you expect the following maps to be combined together:?
Map(€ → List(1, 2, 3), $ → List(4, 5))
Map(€ → List(10, 15), $ → List(100))
The results you would expect is probably Map(€ → List(1, 2, 3, 10, 15), $ → List(4, 5, 11))
, which is only possible because we know how to combine two lists. The Monoid[List[Int]]
I implicitly used here is (Nil, :::)
. For a general type B
you would also need something to smash two B
s together, this something is called a Monoid
!
For completeness, here is the Monoid[Map[A, B]]
I'm guessing the book wants to define:
implicit def mm[A, B](implicit mb: Monoid[B]): Monoid[Map[A, B]] =
new Monoid[Map[A, B]] {
def zero: Map[A, B] = Map.empty
def op(x: Map[A, B], y: Map[A, B]): Map[A, B] =
(x.toList ::: y.toList).groupBy(_._1).map {
case (k, v) => (k, v.map(_._2).reduce(mb.op))
}.toMap
}
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