As an addendum to a previous blog post on the topic of ad-hoc polymorphism in Scala, I’m adding another common typeclass pattern as a separate post. The term “orthogonal” refers to a pattern that selected class attributes are taken out from the base class to form an independent typeclass.
Using an ADT similar to the Car/Sedan/SUV example used in that previous post, we first define `trait Car` as follows:
trait Car { def vin: String def model: String def price: Int }
Unlike how the base trait was set up as a typeclass in the ad-hoc polymorphism example, `trait Car` is now an ordinary trait. But the more significant difference is that method `setPrice()` is no longer in the base class. It’s being constructed “orthogonally” in a designated typeclass:
trait Settable[T] { def setPrice(t: T, amount: Int): T }
Similar to how implicit conversions are set up for ad-hoc polymorphism, implicit values are defined within the companion objects for the individual child classes to implement method `setPrice()` for specific car types.
import scala.language.implicitConversions case class Sedan(vin: String, model: String, price: Int) object Sedan { implicit val carSedan = new Settable[Sedan] { def setPrice(t: Sedan, amount: Int) = t.copy(price = amount) } } case class Sports(vin: String, model: String, price: Int) object Sports { implicit val carSports = new Settable[Sports] { def setPrice(t: Sports, amount: Int) = t.copy(price = amount) } }
The specific method implementations are then abstracted into a “unified” method, `setNewPrice()`, via an implicit constructor argument by passing the `Settable` typeclass into the `CarOps` implicit class:
implicit class CarOps[T](t: T)(implicit ev: Settable[T]) { def setNewPrice(amount: Int) = ev.setPrice(t, amount) }
Testing it out:
val sedan1 = Sedan("1ABC*234", "Honda Accord", 20500) sedan1.setNewPrice(19500) // res1: Sedan = Sedan("1ABC*234", "Honda Accord", 19500.0)
Putting all method implementations in one place
It’s worth noting that having the implicit values for method implementations defined in the companion objects for the individual classes is just one convenient way. Alternatively, these implicit values could all be defined in one place:
trait Car { def vin: String def model: String def price: Int } case class Sedan(vin: String, model: String, price: Int) case class Sports(vin: String, model: String, price: Int) trait Settable[T] { def setPrice(t: T, amount: Int): T } object CarOps { implicit val carSedan = new Settable[Sedan] { def setPrice(t: Sedan, amount: Int) = t.copy(price = amount) } implicit val carSports = new Settable[Sports] { def setPrice(t: Sports, amount: Int) = t.copy(price = amount) } } import CarOps._ implicit class CarOps[T](t: T)(implicit ev: Settable[T]) { def setNewPrice(amount: Int) = ev.setPrice(t, amount) }
A benefit of putting all method implementations in one place is that new methods can be added without touching the base classes – especially useful in situations where those case classes cannot be altered.
For instance, if `color` is also an attribute of `trait Car` and its child case classes, adding a new color setting method will be a trivial exercise by simply adding a `setColor()` method signature in `trait Settable` and its specific method implementations as well as `setNewColor()` within class `CarOps`.
Orthogonal type collection
Let’s see what a collection of cars looks like:
val sedan1 = Sedan("1ABC*234", "Honda Accord", 20500) val sedan2 = Sedan("2DEF*345", "BMW 330i", 38000) val sports1 = Sports("5MNO*678", "Ford Mustang", 34000) val cars = List(sedan1, sedan2, sports1) // cars: List[Product with java.io.Serializable] = List( // Sedan("1ABC*234", "Honda Accord", 20500.0), // Sedan("2DEF*345", "BMW 330i", 38000.0), // Sports("5MNO*678", "Ford Mustang", 34000.0) // )
To refine the inferred `List[Product with java.io.Serializable]` collection type, we could provide some type hints as shown below:
// Existential type hints import scala.language.existentials val cars = List[(T, Settable[T]) forSome { type T }]( (sedan1, implicitly[Settable[Sedan]]), (sedan2, implicitly[Settable[Sedan]]), (sports1, implicitly[Settable[Sports]]) )