6

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?

melston
  • 2,198
  • 22
  • 39
  • There is missing context in your question. We can't guess the answer without having read the book. Your question should be self-contained and complete for us to answer – Dici Mar 04 '17 at 20:13
  • @melston That approach is probably done better with squants or `joda-money`, libraries which already fully implement all the things you can possibly want. Maybe have a look there first? I know it doesn't directly answer your question but I can't help to think the code smells of future hassle for you. – flavian Mar 04 '17 at 21:29
  • @flavian he's probably just reading a book about design in Scala and tries to understand it. This is a question about the mechanisms of the language, not about the best way to work with currencies in Scala – Dici Mar 04 '17 at 21:58
  • Thanks, all. Yes, I am reading the Manning book "Functional and Reactive Domain Modeling" and what I provided was what was discussed in the book so far. I am not terribly proficient with Scala and this example confuses me, hence the question. – melston Mar 04 '17 at 22:24

1 Answers1

5

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 a Monoid if B is a Monoid

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 Bs 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
  }
Community
  • 1
  • 1
OlivierBlanvillain
  • 7,701
  • 4
  • 32
  • 51
  • Thanks, @OlivierBlanvillain, I think I follow what you are saying. The book does say that Map[A, B] is a Monoid if B is a Monoid, though I don't understand that statement. If there is an implicit conversion of Map[A,B] to Monoid then I am unaware of it but I would expect that you probably have it right, here. – melston Mar 04 '17 at 22:26
  • I guess I don't understand what you mean when you say implicit conversion is not used but then to on to define an implicit conversion. Also, if the Monoid you define is not already a part of Prelude (or somewhere else) then I don't see how the code in the book could work. It is not provided anywhere and seems to assume that the `mm` you have here is implicitly available. In any case, it seems that some kind of implicit must be used or the `m` in the Monoid in the original listing couldn't be created, right? – melston Mar 04 '17 at 23:50
  • Ah, I found it and you are absolutely right. I had to go to the online code repo and scavenge around to find it but there are actually a couple of implicit definitions of `Monoid[Map[K,V]]` that were not mentioned in the book (oddly enough both implemented the same way). Thanks to your answer I had a better idea of what to look for. – melston Mar 05 '17 at 00:08