What are the advantages of OOP subtyping over typeclasses, if any? In other words, now that we have typeclasses, is there any reason to still use OOP subtyping?
PS: I am a Scala programmer.
What are the advantages of OOP subtyping over typeclasses, if any? In other words, now that we have typeclasses, is there any reason to still use OOP subtyping?
PS: I am a Scala programmer.
Personally, I find OOP easier to deal with within the constraints of what it handles well. In other words: in cases where you don’t actually need typeclasses, I find objects easier to understand.
However, this might just be an artifact of the syntactic overhead that the typical typeclass embedding of objects has. If Haskell had syntactic sugar for some common kinds of typeclass patterns, that difference would probably vanish.
What I find more interesting, is the fact that the Haskell community shows that typeclasses are more powerful than objects, since there exists a trivial embedding of objects in typeclasses, but typeclasses can do things objects can’t. The Scala community, however, shows that objects are at least as powerful as typeclasses1, since there exists a trivial embedding of typeclasses in objects.
This seems to indicate that the relationship between the two is much more intimate than commonly thought.
1 See Type Classes as Objects and Implicits by Bruno C.d.S. Oliveira, Adriaan Moors and Martin Odersky, as well as the discussion of that paper on Lambda the Ultimate, especially this nice summary by Paul Snively (emphasis added):
Martin Odersky and team’s design decisions around how to do type classes in a unified OO and FP language continue to bear fascinating fruit. Implicits look less and less like “poor man’s type classes,” and more and more like an improvement upon type classes, in my opinion given a quick read of this paper.
In a language that is not purely functional, subtyping lets you have different side-effects with the same usage; this isn't always easy to achieve with type classes. (You can achieve it, of course; it just seems more awkward to me.)
Also, subtyping can be more efficient--it's a way to cache the information that "X is a Y" without requiring repeated conversion (or compiler heroics to cache that information) of X to Y. For very deep hierarchies, this could possibly be an issue.
At present, the syntactic overhead of Scala type classes is a good bit larger than for subtyping via trait inheritance, as is the potential runtime overhead. Imagine a case where you need to have fifty different types of events conform to an interface to support an event processing engine. Much easier to write
class MyEvent extends Event{
val name = "foo"
}
than
class MyEvent{
val name = "foo"
}
object MyEvent2Event{
implicit def convert(myEvent:MyEvent) = new Event{ val name = myEvent.name}
}
The second form allows a lot more flexibility in terms of post-hoc polymorphism, freedom of naming, and general bad-assery, but typing out those fifty conversion methods and then doing the appropriate imports when the typeclass is needed is going to get to be a right pain. If you don't need the flexibility, it's tough to see the payoff. Plus there's that nagging "new" keyword in the second, which will spawn endless "is this overstressing the garbage-collector" arguments.
The situation is worse for mixin inheritance that introduces mutable state. Consider the following trait, taken from production code:
trait Locking{
private val lock = new ReentrantReadWriteLock()
def withReadLock[T](body: => T):T={
try{
lock.readLock.lock()
body
}finally{
lock.readLock.unlock()
}
}
// same for withWriteLock
}
Incredibly handy use of mixin inheritance, and not really doable with Scala type classes, due to the presence of the "lock" val. Where should it go? If you put it in the adapted class, you lose most of the encapsulation value of the trait. If you put it in the adapter code, the locks no longer protect anything, since you'd be locking on different lock objects every time you're adapted.
One more difference for Scala at least is that chains of subtypes just work, whereas chains of typeclasses are much tricker. If we have types A, B, and C, then if A < B and B < C then necessarily A < C. However, if A <% B and B <% C it is not necessarily the case that A <% C. This is because the Scala compiler will not apply multiple implicit conversions, as otherwise type-inference because difficult and (IIRC) potentially undecideable.