Search code examples
scalascalazshapeless

Creating Monoids for every subclass using Scalaz (or Shapeless)


Is it possible to create monoids for every sub-class? For example,

package currency 

final case class GBP[A: Monoid](amount: A)

object Implicits {
  implicit class CurrencyOps[A: Monoid](a: A) {
    def GBP = currency.GBP(a)
  }

  implicit def gbpMonoid[A: Monoid]: Monoid[GBP[A]] = new Monoid[GBP[A]] {
    override def zero =
      GBP(Monoid[A].zero)

    override def append(f1: GBP[A], f2: => GBP[A]): GBP[A] =
      GBP(Semigroup[A].append(f1.amount, f2.amount))
  }
}

test("GBP support plus") {
  1.GBP |+| 2.GBP shouldBe 3.GBP // passed
}

I would like to add more case classes representing currency (e.g USD, EUR, ..)

sealed trait Currency
final case class GBP[A: Monoid](amount: A) extends Currency
final case class USD[A: Monoid](amount: A) extends Currency
final case class EUR[A: Monoid](amount: A) extends Currency

As a result, I have to implement monoids for new case classes. It's somewhat boilerplate.

implicit class CurrencyOps[A: Monoid](a: A) {
  def GBP = currency.GBP(a)
  def EUR = currency.EUR(a)
  def USD = currency.USD(a)
}

implicit def gbpMonoid[A: Monoid]: Monoid[GBP[A]] = new Monoid[GBP[A]] {
  override def zero =
    GBP(Monoid[A].zero)

  override def append(f1: GBP[A], f2: => GBP[A]): GBP[A] =
    GBP(Semigroup[A].append(f1.amount, f2.amount))
}

implicit def usdMonoid[A: Monoid]: Monoid[USD[A]] = new Monoid[USD[A]] {
  override def zero =
    USD(Monoid[A].zero)

  override def append(f1: USD[A], f2: => USD[A]): USD[A] =
    USD(Semigroup[A].append(f1.amount, f2.amount))
}

implicit def eurMonoid[A: Monoid]: Monoid[EUR[A]] = new Monoid[EUR[A]] {
  override def zero =
    EUR(Monoid[A].zero)

  override def append(f1: EUR[A], f2: => EUR[A]): EUR[A] =
    EUR(Semigroup[A].append(f1.amount, f2.amount))
}

Solution

  • Small suggestions

    First I'd like to propose remove Monoid requirement from case classes, as they will carry implicit value in each Currency instance. Without this requirement your wrappers could be much more efficient and even implemented as value classes:

      sealed trait Currency extends Any
      final case class GBP[A](amount: A) extends AnyVal with Currency
      final case class USD[A](amount: A) extends AnyVal with Currency
      final case class EUR[A](amount: A) extends AnyVal with Currency
    

    Shapeless Implementation

    From here you can build simple implementation via shapeless as required:

    import scalaz._
    import shapeless._
    
    implicit def monoidCurrency[A, C[_] <: Currency]
    (implicit monoid: Monoid[A], gen: Generic.Aux[C[A], A :: HNil]) =
      new Monoid[C[A]] {
        def zero: C[A] = gen.from(monoid.zero :: HNil)
        def append(f1: C[A], f2: => C[A]): C[A] = {
          val x = gen.to(f1).head
          val y = gen.to(f2).head
          gen.from(monoid.append(x, y) :: HNil)
        }
      }
    

    and verify it

    import scalaz.syntax.monoid._
    import scalaz.std.anyVal._
    
    println(2.USD |+| 3.USD) // USD(5)
    

    Further improvements

    You can get rid of shapeless at all. Consider such implementations:

    trait CurrencyUnit{
      def show(amounts: String) = s"$amounts $this"
    }
    
    final case class Currency[A, U <: CurrencyUnit](amount: A) extends AnyVal
    

    CurrencyUnit now is not a matter of class, it's just compile-time type-tag

    implicit case object GBP extends CurrencyUnit
    implicit case object USD extends CurrencyUnit{
      override def show(amounts: String) = s"$$$amounts "
    }
    implicit case object EUR extends CurrencyUnit
    
    implicit class CurrencyOps[A](a: A) {
      def GBP = Currency[A, GBP.type](a)
      def EUR = Currency[A, EUR.type](a)
      def USD = Currency[A, USD.type](a)
    }
    

    which you can configure for your needs

    import scalaz.syntax.show._
    
    implicit def currencyShow[A: Show, U <: CurrencyUnit](implicit unit: U) =
      new Show[Currency[A, U]] {
        override def shows(f: Currency[A, U]) = unit.show(f.amount.shows)
      }
    

    And most important easily derive typeclasses via scalaz.Isomorphism.Iso functionality:

    import Isomorphism._
    
    implicit def currencyIso[A, U <: CurrencyUnit] = new (Currency[A, U] <=> A) {
      def to: (Currency[A, U]) => A = _.amount
      def from: (A) => Currency[A, U] = Currency[A, U]
    }
    
    implicit def currencyMonoid[A: Monoid, U <: CurrencyUnit] =
      new IsomorphismMonoid[Currency[A, U], A] {
        def G: Monoid[A] = implicitly
        def iso: Currency[A, U] <=> A = implicitly
      }
    

    Finally you can verify this solution too

    import scalaz.syntax.monoid._
    import scalaz.std.anyVal._
    
    println((2.USD |+| 3.USD).shows) // $5