Type classes provide a way of achieving ad hoc
polymorphism. We’ll look at
what they are, why they’re useful, how they’re typically encoded in Scala.
We’ll make our own Simple
type class that has a single
operation called simplify
returning a
String
, and provide instances of it for Scala’s
List
and Int
classes, along with our own
class C
. Finally, we’ll add convenient syntax in the Scalaz
style to make it extremely easy to use the simplify
operation. This might be the most hands-on, easy to understand explanation of
type classes you’ve ever encountered!
Rationale
A common way to express polymorphism in Scala is inheritance. This is brittle
because the polymorphic relationship between supertype and subtype must be
defined where the subtype itself is defined. For example, if you have an
Animal
trait, your Dog
class must
declare its relationship as a subtype of Animal
as part of
its definition. This is problematic when you do not own potential subtypes (i.e.
they’re provided by a library). It also tightly couples the subtype to all of
its implementations of its potential supertypes. A given subtype may already
have all the methods needed to implement the behavior of a given supertype but
doesn’t know or care about the supertype at definition time. Though less common,
another problem is a subtype may be able to implement a supertype’s operations
in multiple, distinct ways (e.g. multiplication and addition implementations of
a Monoid over Int
). With inheritance this is impossible.
Scalaz
Let’s start with one of the simplest type classes in Scalaz:
Equal
. Equal
describes “a type safe
alternative to universal equality” (if you’re wondering why this is useful or
necessary, ask me in the comments). Here’s an example of providing an instance
of Equal
over our own class C
that uses
value equality to compare two instances.
class C(val name: String)
implicit val equalC: Equal[C] = new Equal[C] {
override def equal(c1: C, c2: C) = c1.name == c2.name
}
Now let’s say we want a notEqual
method that works on any
A
provided there is evidence of
Equal[A]
:
def notEqual[A: Equal](a1: A, a2: A): Boolean =
!implicitly[Equal[A]].equal(a1, a2)
val c1 = new C("foo")
val c2 = new C("bar")
notEqual(c1, c2)
//=> true
Sidenote for those unfamiliar with context bounds: the context bound [A:
Equal]
is what ensures we have an implicit
Equal[C]
instance available when running notEqual(c1,
c2)
. An equivalent and perhaps more clear implementation
without context bounds would look like this:
def notEqual[A](a1: A, a2: A)(implicit e: Equal[A]) = !e.equal(a1, a2)
notEqual(c1, c2)
//=> true
However, there’s an even more concise way of writing this using context bounds
and Scalaz’ /==
operator for instances of
Equal
, or even its unicode ≠
alias:
def notEqual[A: Equal](a1: A, a2: A): Boolean = a1 /== a2
// or
def notEqual[A: Equal](a1: A, a2: A): Boolean = a1 ≠ a2
Since Scalaz already provides these operators, notEqual
is purely
didactic.
Another common typelcass in Scalaz is Show
. This corresponds
to Haskell’s Show
type class, and is used to indicate a
type that can be represented in some way as a string, e.g. for logging or
printing in a REPL. Here’s an instance for our C
class
example from before.
implicit val showC: Show[C] = new Show[C] {
override def show(c: C) = s"C[name=${c.name}]"
}
Scalaz provides syntax helpers that allow us to simply call
.show
on any type that provides evidence of
Show
. We’ll look at how that works in detail toward the end.
new C("qux").show
//=> res22: scalaz.Cord = C[name=qux]
Simple
Now that we’ve gotten a taste for using a few of Scalaz’ type classes, let’s build our own, along with some syntax helpers in the Scalaz style.
trait Simple[F] {
def simplify(f: F): String
}
This is our type class definition. It defines a single operation
simplify
for a given F
and returns a
String
. Before we provide instances, let’s define a method
that expects a Seq
of Simple
instances
and outputs them separated by newlines.
def manySimple[A](simples: Seq[A])(implicit s: Simple[A]): String =
"Many simples:\n\t" + simples.map(s.simplify).mkString("\n\t")
We can use this to easily try out instances. Let’s start with an instance of
C
:
implicit def simpleC: Simple[C] = new Simple[C] {
override def simplify(c: C) = s"Simplified: ${c.show}"
}
We can manually call this by implicitly obtaining a
Simple[C]
then calling simplify
:
implicitly[Simple[C]].simplify(new C("hello"))
Or we can try out the manySimple
method:
manySimple(Stream(new C("foo"), new C("bar"), new C("qux")))
//=> Many simples:
//=> Simplified: C[name=foo]
//=> Simplified: C[name=bar]
//=> Simplified: C[name=qux]
Let’s try another one: Int
.
implicit val simpleInt: Simple[Int] = new Simple[Int] {
override def simplify(i: Int) = s"Simplified Int with value of $i"
}
This is getting easy, right?
implicitly[Simple[Int]].simplify(123)
//=> Simplified Int with value of 123
We can even provide an instance for List[A]
but only if
A
itself has a Simple
instance:
// Evidence for Simple[List[A]] given evidence of Simple[A]
implicit def simpleList[A: Simple]: Simple[List[A]] = new Simple[List[A]] {
override def simplify(l: List[A]) = {
val simplifyA = implicitly[Simple[A]].simplify _
s"Simplified list:\n${l.map(simplifyA).mkString("\n")}"
}
}
Try it out:
implicitly[Simple[List[Int]]].simplify((1 to 5).toList)
//=> Simplified list:
//=> Simplified Int with value of 1
//=> Simplified Int with value of 2
//=> Simplified Int with value of 3
//=> Simplified Int with value of 4
//=> Simplified Int with value of 5
Hopefully you have a feel for how this works (ignoring how unuseful our
Simple
is IRL). Next, let’s follow Scalaz lead and
provide some convenient syntax for working with our new type class; typing
implicitly[Simple[_]]
over and over again is starting to get
old.
Syntax
Wouldn’t it be nice if we could just call .simplify
on objects which provide
evidence of a Simple
? Well, we can via some neat implicit
tricks. Check it out:
final class SimpleOps[F](val self: F)(implicit val F: Simple[F]) {
final def /^ = F.simplify(self)
final def ⬈ = F.simplify(self)
}
trait ToSimpleOps {
implicit def ToSimpleOps[F](v: F)(implicit F0: Simple[F]) =
new SimpleOps[F](v)
}
object simple extends ToSimpleOps
trait SimpleSyntax[F] {
def F: Simple[F]
implicit def ToSimpleOps(v: F): SimpleOps[F] =
new SimpleOps[F](v)(SimpleSyntax.this.F)
}
// New definition of our Simple typeclass that provides an instance of
// SimpleSyntax
trait Simple[F] { self =>
def simplify(f: F): String
val simpleSyntax = new SimpleSyntax[F] { def F = Simple.this }
}
Here we’ve provided two syntax operators as aliases to
simplify
(because no type class is legit without unicode
operator aliases).
import simple._
1 ⬈
//=> Simplified Int with value of 1
new C("I am C") ⬈
//=> Simplified: C[name=I am C]
List(1,2,3) ⬈
//=> Simplified Int with value of 1
//=> Simplified Int with value of 2
//=> Simplified Int with value of 3
// boring
new C("bar") /^
//=> Simplified: C[name=bar]
Conclusion
Type classes are a powerful method of adding support for a set of operations on an existing type in an ad hoc manner. Because Scala doesn’t encode type classes at the language level, there is a bit of boilerplate and implicit trickery to create your own type classes (compare this with Haskell, where classes are elegantly supported at the language level) and operator syntax.
View the full code listing for this post.
Further reading
- Introduction to Typeclasses in Scala (2013)
- Types and Typeclasses — Learn you a Haskell
- Simalacrum — a modern, concise type class encoding for Scala using annotations
- Typeclassopedia — overview of Haskell’s type classes, many of which are also represented in Scalaz, along with a very useful diagram showing relationships between type classes.