1

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.

davidrpugh
  • 4,363
  • 5
  • 32
  • 46

1 Answers1

1

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)
  )
}
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93