This is more a design question than anything else…
I really like Scala’s case classes and use them often. However, I find that I’m often wrapping in my parameters in Options (or rather, Lift’s Boxes) and setting default values to allow for flexibility and to account that a user might not always specify all the parameters. I think I adopted this practice from.
My question is, is this a reasonable approach? Given that everything may be optional, there can be a lot of boilerplate and checking, to the point whether I wonder whether I’m not just using my case classes like Map[String, Any] and wonder whether I wouldn’t be better off just using a Map.
Let me give you a real example. Here I am modeling a money transfer:
case class Amount(amount: Double, currency: Box[Currency] = Empty)
trait TransactionSide
case class From(amount: Box[Amount] = Empty, currency: Box[Currency] = Empty, country: Box[Country] = Empty) extends TransactionSide
case class To(amount: Box[Amount] = Empty, currency: Box[Currency] = Empty, country: Box[Country] = Empty) extends TransactionSide
case class Transaction(from: From, to: To)
Relatively simple to understand, I think. At this simplest we might declare a Transaction like so:
val t = Transaction(From(amount=Full(Amount(100.0)), To(country=Full(US)))
Already I can imagine you think it’s verbose. And if we specify everything:
val t2 = Transaction(From(Full(Amount(100.0, Full(EUR))), Full(EUR), Full(Netherlands)), To(Full(Amount(150.0, Full(USD))), Full(USD), Full(US)))
On the other hand, despite having to throw Full around everywhere, you can still do some nice pattern matching:
t2 match {
case Transaction(From(Full(Amount(amount_from, Full(currency_from1))), Full(currency_from2), Full(country_from)), To(Full(Amount(amount_to, Full(currency_to1))), Full(currency_to2), Full(country_to))) if country_from == country_to => Failure("You're trying to transfer to the same country!")
case Transaction(From(Full(Amount(amount_from, Full(currency_from1))), Full(currency_from2), Full(US)), To(Full(Amount(amount_to, Full(currency_to1))), Full(currency_to2), Full(North_Korea))) => Failure("Transfers from the US to North Korea are not allowed!")
case Transaction(From(Full(Amount(amount_from, Full(currency_from1))), Full(currency_from2), Full(country_from)), To(Full(Amount(amount_to, Full(currency_to1))), Full(currency_to2), Full(country_to))) => Full([something])
case _ => Empty
}
Is this a reasonable approach? Would I be better served by using a Map? Or should I use case classes but in a different fashion? Perhaps using a whole hierarchy of case classes to represent transactions with different amounts of information specified?
Using a case class is less flexible than a map, since you can only assign/access pre-defined fields. You will need to build a full case class hierarchy beforehand.
On the other hand, case class offer kind of “compile-time validations”, because all the types are explicitly defined (in contrast to a
Map[String,Any]), and you cannot assign/access a non specified field by mistake. Case classes should also be faster, because you don’t need to traverse the map hashtable to find what you are looking for.The “verbosity” problem comes from the immutable aspect of case classes, but you’ll have exactly the same problem with immutable maps. The solution seems to be Lenses. There is a very nice talk here:
http://www.youtube.com/watch?v=efv0SQNde5Q&list=PLEDE5BE0C69AF6CCE