Search code examples
scalafunctional-programmingscala-cats

How monoids generalize over types


I was playing with cats' Monoids in scala when I see that the monoid operations are extended for Tuples in a natural way:

import cats.Monoid

object mon {
  implicit object IntMonoid extends Monoid[Int] {
    def combine(a: Int, b: Int) = a*a + b*b
    def empty = 0
  }

  implicit object ListMonoid extends Monoid[List[Int]] {
    def combine(a: List[Int], b: List[Int]): List[Int] =
      a.zip(b).map(z => z._1 * z._2)
    def empty = List(1)
  }

  def comb[T](a: T, b: T)(implicit m: Monoid[T]) =
    m.combine(a, b)
}

val list1 = List(1, 2, 3)
val list2 = List(2, 3, 4)
println(mon.comb(list1, list2)) // outputs: List(2, 6, 12) as expected

val int1 = 2
val int2 = 4
println(mon.comb(int1, int2)) // outputs: 20 as expected

val x = (list1, int1)
val y = (list2, int2)
println(mon.comb(x, y)) // outputs: (List(2, 6, 12),20)

The last output is expected in a 'natural' way, but how does de compiler knows how to do it? I've been trying to look for it in Cats' source code, but I'm not as experienced in Scala as to be able to know what to look for. I suppose the same methods holds for similar constructions like semigroups.


Solution

  • Your question boils down to how implicit derivation of typeclasses for generic types work; so let's see two examples:

    A case where we want to provide an instance no matter what the generic is:

    // Similar to the code you had, but without being tied to just List[Int],
    // Since in this case the Int part is irrelevant.
    implicit def monoidList[A]: Monoid[List[A]] =
      new Monoid[List[A]] {
        override final val empty: List[A] = Nil
    
        override final def combine(l1: List[A], l2: List[A]): List[A] =
          l1 ::: l2
      }
    

    A case where we require a proof of the generic type to provide the instance of the complex type:

    implicit def optionMonoid[A](implicit aMonoid: Monoid[A]): Monoid[Option[A]] =
      new Monoid[Option[A]] {
        override final val empty: Option[A] = None
    
        override final def combine(o1: Option[A], o2: Option[A]): Option[A] =
          (o1, o2) match {
            case (None, None)         => None
            case (Some(a), None)      => Some(a)
            case (None, Some(a))      => Some(a)
            case (Some(a1), Some(a1)) => Some(aMonoid.combine(a1, a2))
          }
      }
    

    Thus, you can now imagine how the Monoid[Tuple2[A, B]] of cats works, but just for completeness the code would be like this:

    implicit def tuple2Monoid[A, B](implicit aMonoid: Monoid[A], bMonoid: Monoid[B]): Monoid[(A, B)] =
      new Monoid[(A, B)] {
        override final def empty: (A, B) =
          (aMonoid.empty, bMonoid.empty)
    
        override final def combine(t1: (A, B), t2: (A, B)): (A, B) =
          (t1, t2) match {
            case ((a1, b1), (a2, b2)) => (aMonoid.combine(a1, a2), bMonoid.combine(b1, b2))
          }
      }