Why Typeclasses
Alex Westphal · 07 Sep 2014A common argument I hear when discussing ad-hoc polymorphism and typeclasses is “But I could do that with inheritance”. While inheritance based polymorphism can replace ad-hoc polymorphism in a lot of cases, there are also a large number of cases where it can’t achieve the same result.
Show: Typeclass vs Inheritance
The Show typeclass (which I described in The Show Typeclass in Scala)
describes a class of types which is showable. It could alternatively be described by an interface. In either case it
essentially boils down to function of type A => String
where A
is the showable type.
Using inheritance based polymorphism we can describe a type as Showable:
// Showable Interface
trait Showable { def show: String }
// Class Bar that is showable
class Bar extends Showable {
def show: String = ...
}
// Usage
def foo(a: Show) = a.show
The is reasonably succinct compared to the typeclass version:
class Bar // Define Class Bar
// Define the typeclass
trait Show[A] { def show(a: A): String }
// Declare an instance of Show for type A
implicit val showBar = new Show[Bar] {
def show(bar: Bar): String = ...
}
// Usage
def foo[A: Show](a: A) = implicitly[Show[A]].show(a)
The obvious advantage of the typeclass version is that it can be applied after the definition of the class rather than at declaration time like inheritance. There are also two other major benefits that aren’t obvious in this case.
Type Constructors
A type constructor is a function that constructs a new type from a old one. For example List[A]
is a type constructor
with A
as its type parameter. It can be applied to a type Bar
to yield a new type List[Bar]
. Some type
constructors can have more than one type parameter, e.g. Function1[A,R]
has two type parameters A
and R
.
In Scala a type constructor is declared as a class. For example List
is declared as:
class List[A]
If we want all types created by the List
type constructor to be showable, we would declare it as:
class List[A] extends Showable
What happens when we feed a type NotShowable
into our type constructor. We get a new type List[NotShowable]
. Should
that new type be showable? Probably not. To fix this we could restrict the types on which our type constructor can be
applied:
class List[A <: Showable] extends Showable
We can no longer have a type like List[NotShowable]
but the primary purpose of the List
type constructor is to
construct types that can be used as lists rather than being showable. Toward that purpose List[NotShowable]
could be
perfectly valid as a list.
Typeclasses provide a better solution because we can use polymorphic methods to define instances of our typeclass for a type constructor:
def showList[A: Show]: Show[List[A]] = ...
This is a sensible declaration because it provides no restriction on List
itself yet a type produced by the List
type constructor is only a member of the Show
type class if and only if the input type is a member of the Show
typeclass. For example List[Bar]
and List[List[Bar]]
are members of the Show
typeclass if and only if Bar
is a
member.
In this way the foo function only accepts the types it can show:
def foo[A: Show](a: A) = implicitly[Show[A]].show(a)
val ls: List[Showable] = ...
val lns: List[NotShowable] = ...
foo(ls) // All good
foo(lns) // Compiler Error
Configurable Implementations
Because typeclass instances are defined separately from their relevant types, we call it ad-hoc polymorphism. A major benefit of being ad-hoc is that we can select different instances depending on our needs.
// Assuming the syntax tricks from the "Show Typeclass in Scala" post
// Define Instances for Tuple1
object ShowTupleInstance {
implicit def showTuple1Parens[A: Show] =
show[A]( case (a) => "(" + a.show + ")" )
implicit def showTuple1Brackets[A: Show] =
show[A]( case (a) => "[" + a.show + "]" )
implicit def showTuple1Braces[A: Show] =
show[A]( case (a) => "{" + a.show + "}" )
}
// Select the implementation we want to use
import ShowTupleInstances.showTuple1Brackets
foo((2)) // Returns "[2]"
Note: This feature of typeclasses is dependent on Scala’s scoping rules and so isn’t available in Haskell.