Search code examples
scalaimplicit-conversionimplicit

Composing Implicit Derivation "Laws"


I'm trying to design a toy CoversionRate implementation to understand how I can encode "laws" through chained implicit definitions. The following code compiles:

case class ConversionRate[X <: Currency, Y <: Currency](rate: Double) extends AnyVal
object ConversionRate {
  implicit val EUR2USD: ConversionRate[EUR.type, USD.type] = ConversionRate(1.1)
  implicit val GBP2USD: ConversionRate[GBP.type, USD.type] = ConversionRate(1.21)

  // "Laws"
  implicit def inverse[X <: Currency, Y <: Currency](implicit cr: ConversionRate[X, Y]): ConversionRate[Y, X] =
    ConversionRate[Y, X](1 / cr.rate)

  implicit def transitivity[X <: Currency, Z <: Currency](implicit cr: ConversionRate[X, USD.type], cr2: ConversionRate[Z, USD.type]): ConversionRate[X, Z] =
    ConversionRate(cr.rate * cr2.rate)

  private val unit = ConversionRate(1)
  implicit def self[X <: Currency]: ConversionRate[X,X] = unit.asInstanceOf
}

The call-site derivation does however not work

val cr = implicitly[ConversionRate[EUR.type, GBP.type]]
diverging implicit expansion for type ConversionRate[EUR.type,GBP.type]
starting with method transitivity in object ConversionRate

How should I go about writing the laws in a way that they can used for derivation?


Solution

  • After looking a bit more into it, I think I found an even simpler solution, using lower priority implicits instead of shapeless operators to break the implicit cycles.

    case class ConversionRate[X <: Currency, Y <: Currency](rate: Double) extends AnyVal
    trait ConversionRateLowPriorityImplicits {
      implicit def transitivity[X <: Currency, Z <: Currency](implicit cr: ConversionRate[X, USD.type], cr2: ConversionRate[Z, USD.type]): ConversionRate[X, Z] =
        ConversionRate(cr.rate * cr2.rate)
    }
    object ConversionRate extends ConversionRateLowPriorityImplicits {
      implicit val EUR2USD: ConversionRate[EUR.type, USD.type] = new ConversionRate(1.1)
      implicit val GBP2USD: ConversionRate[GBP.type, USD.type] = new ConversionRate(1.21)
    
      // "Laws"
      implicit def inverse[X <: Currency, Y <: Currency](implicit cr: ConversionRate[X, Y]): ConversionRate[Y, X] =
        new ConversionRate[Y, X](1 / cr.rate)
    
      private val unit = new ConversionRate(1)
      implicit def self[X <: Currency]: ConversionRate[X,X] = unit.asInstanceOf
    }
    

    All the following now resolve nicely

    implicitly[ConversionRate[EUR.type, EUR.type]]
    implicitly[ConversionRate[EUR.type, USD.type]]
    implicitly[ConversionRate[EUR.type, GBP.type]]
    implicitly[ConversionRate[USD.type, USD.type]]
    implicitly[ConversionRate[USD.type, EUR.type]]
    implicitly[ConversionRate[USD.type, GBP.type]]
    implicitly[ConversionRate[GBP.type, GBP.type]]
    implicitly[ConversionRate[GBP.type, EUR.type]]
    implicitly[ConversionRate[GBP.type, USD.type]]