Ad-hoc polymorphism and type classes

Types of polymorphism

We’ll start from parametric polymorphism. Say we have some list of items; this could be a list of integers, doubles, strings, whatever. Now consider a method head() which returns the first item from that list. This method doesn’t care if the item is of type Int, String, Apple or Orange. Its return type is the one list is parameterized with and its implementation is the same for all types: “return first item”.

How to append ducks

So, we want the appendItems() method to take two instances of something “appendable” and perform the append operation upon them. We also want this operation to have different implementations for different appendables. For integers append means addition and for strings it means concatenation. This is ad-hoc polymorphism at its best.

def appendItems[A](a: A, b: A) = a append b

Implicit conversions

To assure the compiler that A will really be appendable, we can use an implicit parameter, a common mechanism in Scala, to provide a conversion:

def appendItems[A](a: A, b: A)(implicit ev: A => Appendable[A]) =
a append b
trait Appendable[A] {
def append(a: A): A
}
class AppendableInt(i: Int) extends Appendable[Int] { 
override def append(a: Int) = i + a
}
class AppendableString(s: String) extends Appendable[String] {
override def append(a: String) = s.concat(a)
}
implicit def toAppendable(i: Int) = new AppendableInt(i)
implicit def toAppendable(s: String) = new AppendableString(s)
trait Appendable[A] {
def append(a: A): A
}

class AppendableInt(i: Int) extends Appendable[Int] {
override def append(a: Int) = i + a
}

class AppendableString(s: String) extends Appendable[String] {
override def append(a: String) = s.concat(a)
}

implicit def toAppendable(i: Int) = new AppendableInt(i)
implicit def toAppendable(s: String) = new AppendableString(s)

def appendItems[A](a: A, b: A)(implicit ev: A => Appendable[A]) =
a append b

val res1 = appendItems(2, 3) // res1 is an Int with value 5
val res2 = appendItems("2", "3") // res2 is a String with value "23"

Type classes

Implicit conversions were fun. Type classes are even more fun — they are more flexible and therefore more powerful. But instead of taking my word for it, read on and see for yourself.

def appendItems[A](a: A, b: A)(implicit ev: Appendable[A]) = 
a append b
trait Appendable[A] {
def append(a: A, b: A): A
}
object Appendable {
implicit val appendableInt = new Appendable[Int] {
override def append(a: Int, b: Int) = a + b
}
implicit val appendableString = new Appendable[String] {
override def append(a: String, b: String) = a.concat(b)
}
}
def appendItems[A](a: A, b: A)(implicit ev: Appendable[A]) =
ev.append(a, b)
val res1 = appendItems(2, 3) // returns 5
val res2 = appendItems("2", "3") // returns "23"
trait Appendable[A] {
def append(a: A, b: A): A
}

object Appendable {
implicit val appendableInt = new Appendable[Int] {
override def append(a: Int, b: Int) = a + b
}
implicit val appendableString = new Appendable[String] {
override def append(a: String, b: String) = a.concat(b)
}
}

implicit val appendableInt2 = new Appendable[Int] {
override def append(a: Int, b: Int) = a * b
}


def appendItems[A](a: A, b: A)(implicit ev: Appendable[A]) =
ev.append(a, b)

val res1 = appendItems(2, 3) // returns 6

View and context bounds

Ad-hoc polymorphism has been explained and implemented with two different approaches: implicit conversions and type classes. Now, there are two constructs in Scala, namely view bounds and context bounds, that provide some extra syntax sugar for working with those two approaches.

// def appendItems[A](a: A, b: A)(implicit ev: A => Appendable[A]) = 
// a append b
def appendItems[A <% Appendable[A]](a: A, b: A) = a append b
// def appendItems[A](a: A, b: A)(implicit t: Appendable[A]) =
// t.append(a, b)
def appendItems[A : Appendable](a: A, b: A) =
implicitly[Appendable[A]].append(a, b)

The big question — who wins?

Actually, I already spoiled the answer to that question by mentioning that type classes provide us with more flexibility and power. If you payed enough attention, you may have even realized that yourself by now (hint: type class can have *multiple* implementations for the same type). But let’s back that up with a real world showcase.

def sorted[B >: A](implicit ord: math.Ordering[B]): Seq[A]

Conclusion

As I said, you can still use implicit conversions for simple stuff. They’re fine for specific, non-modular, ad-hoc conversions. For example, if you have a standard MVC stack where controller uses services which operate on the database, you may want to automatically transform each database response into service response, and service response into controller response. No need for type classes there. However, when writing reusable, modular, generalized code that will be used and extended by others — gravitate towards type classes.

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store