Tag Archives: ad-hoc polymorphism

Orthogonal Typeclass In Scala

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]])
  )

Ad-hoc Polymorphism In Scala

Over the past few years, there seems to be a subtle trend of software engineers favoring typeclass patterns that implement polymorphism in an ad-hoc fashion, namely, Ad-hoc Polymorphism. To see the benefits of such kind of polymorphism, let’s first look at what F-bounded polymorphism, a subtype polymorphism, has to offer.

F-bounded polymorphism

// F-bounded polymorphism
trait Car[T <: Car[T]] { self: T =>
  def vin: String
  def model: String
  def price: Double
  def setPrice(newPrice: Double): T
}

Next, a couple of child classes are defined:

// F-bounded polymorphism continued
case class Sedan(vin: String, model: String, price: Double) extends Car[Sedan] {
  def setPrice(newPrice: Double) = copy(price = newPrice)
}

case class Sports(vin: String, model: String, price: Double) extends Car[Sports] {
  def setPrice(newPrice: Double) = copy(price = newPrice)
}

A F-bounded type has a peculiar signature of the self-recursive `A[T <: A[T]]` which mandates the given type `T` itself a sub-type of `A[T]`, like how type `Sedan` is defined (Sedan <: Car[Sedan]). Note that the self-type annotation used in the trait isn’t requirement for F-bounded type. Rather, it’s a common practice for safeguarding against undesirable mix-up of sub-classes like below:

// F-bounded polymorphism with self-type
case class Sports(vin: String, model: String, price: Double) extends Car[Sedan] {
  def setPrice(newPrice: Double) = Sedan(vin, model, newPrice)
}
// ERROR: illegal inheritance; self-type Sports does not conform to Car[Sedan]'s selftype Sedan ...

“Type argument” versus “Type member”

Rather than a `type argument`, a F-bounded type could also be expressed as a `type member` which needs to be defined in its child classes.:

// F-bounded polymorphism expressed as type member
trait Car {
  type T <: Car
  def vin: String
  def model: String
  def price: Double
  def setPrice(newPrice: Double): T
}

case class Sedan(...) extends Car {
  type T = Sedan
  ...
}

case class Sports(...) extends Car {
  type T = Sports
  ...
}

It should be noted that with the `type member` approach, self-type would not be applicable, hence mix-up of sub-classes mentioned above is possible.

Let’s define a sedan and test out method `setPrice`:

// F-bounded polymorphism example continued
val sedan1 = Sedan("1ABC*234", "Honda Accord", 20500)

sedan1.setPrice(19500)
// res1: Sedan = Sedan("1ABC*234", "Honda Accord", 19500.0)

Under the F-bounded type’s “contract”, a method such as the following would work as intended to return the specified sub-type:

Had the Car/Sedan hierarchy been set up as the less specific `T <: Car`, the corresponding method:

// Problem with simple subtype
def setSalePrice[T <: Car](car: T, discount: Double): T =
  car.setPrice(car.price * (1.0 - discount max 0.0))

would fail as it couldn’t guarantee the returning type is the exact type of the input.

F-bounded type collection

Next, let’s look at a collection of cars.

// Collection of F-bounded elements
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 Serializable with Car[_ >: Sports with SUV with Sedan <: Product with Serializable with Car[_ >: Sports with SUV with Sedan <: Product with Serializable]]] = ...

The resulting type is a rather ugly sequence of gibberish. To help the compiler a little, give it some hints about `T <: Car[T]` as shown below:

// Existential type hints
import scala.language.existentials

val cars = List[T forSome { type T <: Car[T] }](sedan1, sedan2, sports1)
// cars: List[T forSome { type T <: Car[T] }] = List(...)

Ad-hoc polymorphism

Contrary to subtype polymorphism which orients around a supertype with a rigid subtype structure, let’s explore a different approach using typeclasses, known as Ad-hoc polymorphism.

// Ad-hoc polymorphism example
trait Car[T] {
  def vin(car: T): String
  def model(car: T): String
  def price(car: T): Double
  def setPrice(car: T, newPrice: Double): T
}

case class Sedan(vin: String, model: String, price: Double)
case class Sports(vin: String, model: String, price: Double)

Next, a couple of “ad-hoc” implicit objects are created to implement the trait methods.

// Ad-hoc polymorphism example continued
import scala.language.implicitConversions

implicit object SedanCar extends Car[Sedan] {
  def vin(car: Sedan) = car.vin
  def model(car: Sedan) = car.model
  def price(car: Sedan) = car.price
  def setPrice(car: Sedan, newPrice: Double): Sedan = car.copy(price = newPrice)
}

implicit object SportsCar extends Car[Sports] {
  def vin(car: Sports) = car.vin
  def model(car: Sports) = car.model
  def price(car: Sports) = car.price
  def setPrice(car: Sports, newPrice: Double): Sports = car.copy(price = newPrice)
}

Note that alternatively, the implicit objects could be set up as ordinary companion objects of the case classes with implicit anonymous classes:

// Alternative - defining implicits in companion objects
case class Sedan(vin: String, model: String, price: Double)
object Sedan {
  implicit val SedanCar = new Car[Sedan] {
    def vin(car: Sedan) = car.vin
    def model(car: Sedan) = car.model
    def price(car: Sedan) = car.price
    def setPrice(car: Sedan, newPrice: Double): Sedan = car.copy(price = newPrice)
  }
}

case class Sports(vin: String, model: String, price: Double)
object Sports {
  implicit val SportsCar = new Car[Sports] {
    def vin(car: Sports) = car.vin
    def model(car: Sports) = car.model
    def price(car: Sports) = car.price
    def setPrice(car: Sports, newPrice: Double): Sports = car.copy(price = newPrice)
  }
}

Unifying implemented methods

Finally, an implicit conversion for cars of type `T` is provided by means of an implicit class to create a “unified” method that takes the corresponding method implementations from the provided implicit `Car[T]` parameter.

// Ad-hoc polymorphism example continued
implicit class CarOps[T](car: T)(implicit ev: Car[T]) {
  def setPrice(newPrice: Double): T = ev.setPrice(car: T, newPrice: Double)
}

Testing it out:

// Ad-hoc polymorphism example continued
val sedan1 = Sedan("1ABC*234", "Honda Accord", 20500)

sedan1.setPrice(19500)
// res1: Sedan = Sedan("1ABC*234", "Honda Accord", 19500.0)

New methods, like `setSalePrice`, can be added as needed in the implicit objects:

// Ad-hoc polymorphism example continued
implicit object SedanCar extends Car[Sedan] {
  ...
  def setSalePrice(car: Sedan, discount: Double): Sedan =
    car.setPrice(car.price * (1.0 - discount max 0.0))
}

implicit object SportsCar extends Car[Sports] {
    ...
  def setSalePrice(car: Sports, discount: Double): Sports =
    car.setPrice(car.price * (1.0 - discount max 0.0))
}

Ad-hoc type collection

Next, a collection of cars:

// Collection of elements of ad-hoc type
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)
// )

Similar to the F-bounded collection, the inferred resulting type isn’t very helpful. Unlike in the F-bounded case, we do not have a `T <: Car[T]` contract. Using an approach illustrated in this blog post, we could assemble the collection as a list of `(car, type)` tuples:

// Existential type hints
import scala.language.existentials

val cars = List[(T, Car[T]) forSome { type T }](
  (sedan1, implicitly[Car[Sedan]]),
  (sedan2, implicitly[Car[Sedan]]), 
  (sports1, implicitly[Car[Sports]])
)
// cars: List[(T, Car[T] forSome { type T })] = List(...)

By means of a simple example, we’ve now got a sense of how Ad-hoc polymorphism works. The F-bounded example serves as a contrasting reference of how the polymorphism bound by a more “strict” contract plays out in comparison. Given the flexibility of not having to bind the base classes into a stringent subtype relationship upfront, the rising popularity of Ad-hoc polymorphism certainly has its merits.

That said, lots of class models in real-world applications still fits perfectly well into a subtype relationship. In suitable use cases, F-bounded polymorphism generally imposes less boilerplate code. In addition, Ad-hoc polymorphism typically involves using of implicits that may impact code maintainability.