Type classes

Ad-hoc polymorphism is especially useful in languages that aren't object-oriented and thus can't have subtype polymorphism. One example of such a language is Haskell. The pattern we're discussing is named type classes in Haskell and this name also came over to Scala. Type classes are widely used in stdlib and open source libraries and are fundamental for functional programming in Scala.

The name type classes sound very familiar for an object-oriented developer because of the notion of classes. Unfortunately, it has nothing to do with classes in the OO sense and is just confusing. It helped me to think about type classes as a class of types instead in order to rewire my brain for this pattern.

Let's compare it to the traditional object-oriented approach and a type class that is used to define a set of USB cables. With OO, we would have the following definition:

trait Cable {
def connect(): Boolean
}
case class Usb(orientation: Boolean) extends Cable {
override def connect(): Boolean = orientation
}
case class Lightning(length: Int) extends Cable {
override def connect(): Boolean = length > 100
}
case class UsbC(kind: String) extends Cable {
override def connect(): Boolean = kind.contains("USB 3.1")
}
def connectCable(c: Cable): Boolean = c.connect()

Each of the subclasses implements the connect method by overriding the base traits' method. The connectCable just delegates the call to the instance, and proper implementation is called using dynamic dispatch:

scala> connectCable(Usb(false))
res9: Boolean = false
scala> connectCable(Lightning(150))
res10: Boolean = true

The type class version looks slightly different. The classes do not need to extend the Cable any more (and thus are free to be a part of a different class hierarchy). We've also made a UsbC type generic, just for fun:

case class Usb(orientation: Boolean)
case class Lightning(length: Int)
case class UsbC[Kind](kind: Kind)

The connection logic has moved into the type class that's been parameterized by the type of the cable:

trait Cable[C] {
def connect(c: C): Boolean
}

It is implemented in the respective type class instances:

implicit val UsbCable: Cable[Usb] = new Cable[Usb] {
override def connect(c: Usb): Boolean = c.orientation
}

Or in the same approach using single abstract method syntax:

implicit val LightningCable: Cable[Lightning] = (_: Lightning).length > 100

We can't just define an implicit instance for our recently parameterized UsbC as we can't provide a generic implementation for any type parameter. The instance for the UsbC[String] (the same as in the OO version) can be easily implemented through the following:

implicit val UsbCCableString: Cable[UsbC[String]] = 
(_: UsbC[String]).kind.contains("USB 3.1")

The connectCable is implemented with the context bound and uses ad-hoc polymorphism to select a proper delegate method:

def connectCable[C : Cable](c: C): Boolean = implicitly[Cable[C]].connect(c)

This method can be called in the same way we called its OO sibling:

scala> connectCable(Usb(false))
res11: Boolean = false
scala> connectCable(Lightning(150))
res12: Boolean = true
scala> connectCable(UsbC("USB 3.1"))
res13: Boolean = true

On the call side, we have the same syntax, but the implementation is different. It is completely decoupled—our case classes don't know anything about the connection logic. In fact, we could have implemented this logic for the classes defined in another, closed source library!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.44.182