Search code examples
scalapattern-matchingalgebraic-data-types

Scala: Refined Algebraic Data Types


o/

This might be a rather interesting question, and one that might spark some creativity among you.

I want to model currencies in a way that I can:

  • pattern match over the type (=> algebraic data type)
  • store a numeric amount in it
  • use a refined type for constraining the value to e.g. positive values, like val amount: Float Refined Positive
  • have a three character currency code like "USD" that is predefined and immutable

Doing a subset of this in one implementation is easy, but I found it surprisingly hard to create a type that allows for something like the following:

def doSomething(currency: Currency): Unit {
  currency match {
    case BITCOIN => println("Oh, a cryptocurrency! And it is ${currency.amount} ${currency.code}!"
    case EURO => println("So we are from Europe, eh?")
  }
}

doSomething(new Currency.BITCOIN(123f)) // yielding "Oh, a cryptocurrency! And it is 123 BTC!"

val euro = new Currency.EURO(-42f) // compile error

I hope I made my intentions clear. If there is a library doing that, I'm happy to be pointed at it, though I hope to learn something from thinking about this myself.


Solution

  • Do you mean something like this?

    import eu.timepit.refined.api.Refined
    import eu.timepit.refined.auto._
    import eu.timepit.refined.numeric.NonNegative
    import eu.timepit.refined.string.MatchesRegex
    
    sealed trait Currency extends Product with Serializable {
      def amount: Currency.Amount
      def code: Currency.Code
    }
    
    object Currency {
      type Amount = BigDecimal Refined NonNegative
      type Code = String Refined MatchesRegex["[A-Z]{3}"]
    
      final case class Euro(amount: Amount) extends Currency {
        override final val code: Code = "EUR"
      }
    
      final case class Dollar(amount: Amount) extends Currency {
        override final val code: Code = "USD"
      }
    }
    
    def doSomething(currency: Currency): Unit =
      currency match {
        case Currency.Euro(amount) => println(s"Euro: € ${amount}")
        case _ => println(s"Somenthing else with code ${currency.code} and amount ${currency.amount}")
      }
    

    This works:

    doSomething(Currency.Dollar(BigDecimal(10))) 
    // Somenthing else with code USD and amount 10