This is a function that uses the Monad
typeclass by a context bound:
def f[A : Monad](x:A) = ???
( Yeah, we get flatMap method now)
This, however, uses inheritance with a subtype bound:
def f[A <: Monad](x:A) = ???
f(x) // where x is a CatsList which implements Monad trait.
( We also get flatMap method now.)
Don't both achieve the same thing?
Typeclasses are more flexible. In particular, it's possible to retrofit a typeclass to an existing type easily. To do this with inheritance, you would need to use the adapter pattern, which can get cumbersome when you have multiple traits.
For example, suppose you had two libraries which added the traits Measurable
and Functor
, respectively, using inheritance:
trait Measurable {
def length: Int
}
trait Functor[A] {
def map[B](f: A => B): Functor[B]
}
The first library helpfully defined an adapter for List => Measurable:
class ListIsMeasurable(ls: List[_]) extends Measurable {
def length = ls.length
}
And the second library did the same for List => Functor. Now we want to write a function that takes things that have both length and map methods:
def foo(x: Measurable with Functor) = ???
Of course, we should hope to be able to pass it a List
. However, our adapters are useless here, and we have to write yet another adapter to make List
conform to Measurable with Functor
. In general, if you have n
interfaces that might apply to List
, you have 2^n
possible adapters. On the other hand, if we had used typeclasses, there would have been no need for the extra boilerplate of a third typeclass instance:
def foo[A : Measurable : Functor](a: A) = ???