4

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.

Dioni
  • 118
  • 7
  • 2
    I copy/pasted your whole example and got this error: `error: could not find implicit value for parameter m: Monoid[(List[Int], Int)]`. There must be an instance for these types somewhere. – Branislav Lazic Sep 29 '21 at 18:22
  • I think you are mixing your own `Monoid` with **cats** one is that correct? Anyways, **cats** provides an instance for tuples the same way it provides an instance for any `List` or any `Option` by using implicit derivation and by requiring proof that the types of the tuple have also `Monoids` instances associated with them. - Maybe your question was that? How to encode a **typeclass** derivation of a complex type? – Luis Miguel Mejía Suárez Sep 29 '21 at 18:30
  • Yes @LuisMiguelMejíaSuárez, I am asking for how the things are implemented to let the compiler know that if I only provide monoidal operations for those two types, when I call comb function it has to act individually over each component. – Dioni Sep 29 '21 at 19:21
  • 1
    @Dioni the code you're looking for gets generated here: https://github.com/typelevel/cats/blob/b19167169c46fa9182a89f20829bb60df4392afd/project/KernelBoiler.scala#L135 – Ionuț G. Stan Oct 01 '21 at 10:45

1 Answers1

2

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))
      }
  }
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • 1
    Just to be more precise, the implementation of that behaviour can be found in [cats code](https://github.com/typelevel/cats/blob/b19167169c46fa9182a89f20829bb60df4392afd/core/src/main/scala/cats/data/Tuple2K.scala#L321) – Dioni Sep 29 '21 at 21:05
  • 1
    @Dioni that's the code for `MonoidK`, not `Monoid`. If I remember correctly, the code for the tuple instances is generated by an sbt plugin in cats kernel. – Levi Ramsey Sep 30 '21 at 00:39
  • This still somehow does not really answer "how does de compiler knows how to do it?", a link to the actual `cats` implementations, and a hint about how to find it in the docs would be helpful. – Andrey Tyukin Oct 01 '21 at 10:26
  • 2
    @AndreyTyukin Tuple instances for `Monoid` are generated here: https://github.com/typelevel/cats/blob/b19167169c46fa9182a89f20829bb60df4392afd/project/KernelBoiler.scala#L135 There are a few other boilerplate generation places here: https://github.com/typelevel/cats/tree/b19167169c46fa9182a89f20829bb60df4392afd/project – Ionuț G. Stan Oct 01 '21 at 10:40