Typeclasses and inheritance enable reuse in different ways. Inheritance excels at providing correct functionality for changed internals.
class Foo { def foo: String = "foo" }
def fooUser(foo: Foo) { println(foo.foo) }
class Bar extends Foo {
private var annotation = List.empty[String]
def annotate(s: String) { annotation = s :: annotation }
override def foo = ("bar" :: annotation.map("@" + _)).mkString(" ")
}
Now, everyone who uses Foo
will be able to get the correct value if you give them a Bar
, even if they only know that the type is a Foo
. You don't have to have anticipated that you might want pluggable functionality (except by not labeling foo
final
). You don't need to keep track of the type or keep passing a witness instance forwards; you just use Bar
wherever you want in place of Foo
and it does the right thing. This is a big deal. If you want a fixed interface with easily-modifiable functionality under the hood, inheritance is your thing.
In contrast, inheritance is not so great when you have a fixed set of data types with easily-modifiable interface. Sorting is a great example. Suppose you want to sort Foo
. If you try
class Foo extends Sortable[Foo] {
def lt(you: Foo) = foo < you.foo
def foo = "foo"
}
you could pass this to anything that could sort a Sortable
. But what if you want to sort by length of name not with the standard sort? Well,
class Foo extends LexicallySortable[Foo] with LengthSortable[Foo] {
def lexicalLt(you: Foo) = foo < you.foo
def lengthLt(you: Foo) = foo.length < you.foo.length
def foo = "foo"
}
This rapidly becomes hopeless, especially since you have to hunt down all subclasses of Foo and make sure they are updated properly. You are much better off deferring the less-than computation to a typeclass which you can swap out as needed. (Or to a regular class, which you must always reference explicitly.) This kind of automatically-selected functionality is also a big deal.
You can't really replace one with the other. When you need to easily incorporate new kinds of data to a fixed interface, use inheritance. When you need a few kinds of underlying data but need to easily supply new functionality, use type classes. When you need both, you will have a lot of work to do whichever way you go about it, so use to taste.