Search code examples
scalafunctional-programmingscala-catsmonoids

Idiomatic approach to "composing" monoids using cats?


I would like to "compose" two monoids using cats. If there exists a defined Monoid[(A, A) => Int], then I would like to be able to create a Monoid[Preference[A]] using the combine and empty methods of the Monoid[(A, A) => Int]. I am using the term "composing" loosely here because I am not sure that the transform I want to do is accurately called composition.

Here is my current attempt...

import cats._
import cats.implicits._

trait Preference[A] extends Order[A]  

object Preference {

  def from[A](f: (A, A) => Int): Preference[A] = {
    new Preference[A] {
      def compare(a1: A, a2: A): Int = {
        f(a1, a2)
      }
    }
  }

  def monoid[A](implicit ev: Monoid[(A, A) => Int]): Monoid[Preference[A]] = {
    new Monoid[Preference[A]] {
      def combine(p1: Preference[A], p2: Preference[A]): Preference[A] = {
        new Preference[A] {
          def compare(a1: A, a2:A): Int = {
            ev.combine(p1.compare, p2.compare)(a1, a2)
          }
        }
      }
      def empty: Preference[A] = {
        from(ev.empty)
      }
    }
  }
}

...this compiles but I would like to know if there is a more idiomatic solution available using cats.

Seems like it should be possible to somehow compose the Monoid[(A,A) => Int] with the from combinator that takes a f:(A, A) => Int and returns a Preference[A] to create a Monoid[Preference[A]] but I can not figure out how to do it.

I have seen this SO post which discusses composing monoids using a product combinator which is not what I want.


Solution

  • I'm not aware of anything built-in into cats directly.

    It seems that you have an isomorphism between Preference[A] and (A, A) => Int, and you simply want to transfer the monoid-structure from (A, A) => Int to Preference[A]. This can be expressed generically for arbitrary types A and B:

    def fromIsomorphicMonoid[A, B](
      forward: A => B,
      inverse: B => A
    )(implicit aMon: Monoid[A]): Monoid[B] = new Monoid[B] {
      def combine(b1: B, b2: B): B = 
        forward(aMon.combine(inverse(b1), inverse(b2)))
      def empty: B = forward(aMon.empty)
    }
    

    With this helper method, your monoid in Preference becomes just:

    def monoid[A](implicit ev: Monoid[(A, A) => Int]): Monoid[Preference[A]] =
      fromIsomorphicMonoid(
        from, 
        (p: Preference[A]) => (x:A, y:A) => p.compare(x, y)
      )
    

    Full compilable example (without any dependencies):

    trait Monoid[X] {
      def empty: X
      def combine(x: X, y: X): X
    }
    
    trait Order[A] {
      def compare(a1: A, a2: A): Int
    }
    
    def fromIsomorphicMonoid[A, B](
      forward: A => B,
      inverse: B => A
    )(implicit aMon: Monoid[A]): Monoid[B] = new Monoid[B] {
      def combine(b1: B, b2: B): B = 
        forward(aMon.combine(inverse(b1), inverse(b2)))
      def empty: B = forward(aMon.empty)
    }
    
    trait Preference[A] extends Order[A]  
    
    object Preference {
    
      def from[A](f: (A, A) => Int): Preference[A] = {
        new Preference[A] {
          def compare(a1: A, a2: A): Int = {
            f(a1, a2)
          }
        }
      }
    
      def monoid[A](implicit ev: Monoid[(A, A) => Int])
      : Monoid[Preference[A]] = fromIsomorphicMonoid(
        from, 
        (p: Preference[A]) => (x:A, y:A) => p.compare(x, y)
      )
    }