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.
_10class C(val name: String)_10_10implicit val equalC: Equal[C] = new Equal[C] {_10 override def equal(c1: C, c2: C) = c1.name == c2.name_10}
Now let's say we want a notEqual
method that works on any
A
provided there is evidence of
Equal[A]
:
_10def notEqual[A: Equal](a1: A, a2: A): Boolean =_10 !implicitly[Equal[A]].equal(a1, a2)_10_10val c1 = new C("foo")_10val c2 = new C("bar")_10_10notEqual(c1, c2)_10//=> 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:
_10def notEqual[A](a1: A, a2: A)(implicit e: Equal[A]) = !e.equal(a1, a2)_10notEqual(c1, c2)_10//=> 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:
_10def notEqual[A: Equal](a1: A, a2: A): Boolean = a1 /== a2_10// or_10def 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.
_10implicit val showC: Show[C] = new Show[C] {_10 override def show(c: C) = s"C[name=${c.name}]"_10}
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.
_10new C("qux").show_10//=> 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.
_10trait Simple[F] {_10 def simplify(f: F): String_10}
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.
_10def manySimple[A](simples: Seq[A])(implicit s: Simple[A]): String =_10 "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
:
_10implicit def simpleC: Simple[C] = new Simple[C] {_10 override def simplify(c: C) = s"Simplified: ${c.show}"_10}
We can manually call this by implicitly obtaining a
Simple[C]
then calling simplify
:
_10implicitly[Simple[C]].simplify(new C("hello"))
Or we can try out the manySimple
method:
_10manySimple(Stream(new C("foo"), new C("bar"), new C("qux")))_10//=> Many simples:_10//=> Simplified: C[name=foo]_10//=> Simplified: C[name=bar]_10//=> Simplified: C[name=qux]
Let's try another one: Int
.
_10implicit val simpleInt: Simple[Int] = new Simple[Int] {_10 override def simplify(i: Int) = s"Simplified Int with value of $i"_10}
This is getting easy, right?
_10implicitly[Simple[Int]].simplify(123)_10//=> Simplified Int with value of 123
We can even provide an instance for List[A]
but only if
A
itself has a Simple
instance:
_10// Evidence for Simple[List[A]] given evidence of Simple[A]_10implicit def simpleList[A: Simple]: Simple[List[A]] = new Simple[List[A]] {_10 override def simplify(l: List[A]) = {_10 val simplifyA = implicitly[Simple[A]].simplify __10 s"Simplified list:\n${l.map(simplifyA).mkString("\n")}"_10 }_10}
Try it out:
_10implicitly[Simple[List[Int]]].simplify((1 to 5).toList)_10//=> Simplified list:_10//=> Simplified Int with value of 1_10//=> Simplified Int with value of 2_10//=> Simplified Int with value of 3_10//=> Simplified Int with value of 4_10//=> 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:
_24final class SimpleOps[F](val self: F)(implicit val F: Simple[F]) {_24 final def /^ = F.simplify(self)_24 final def ⬈ = F.simplify(self)_24}_24_24trait ToSimpleOps {_24 implicit def ToSimpleOps[F](v: F)(implicit F0: Simple[F]) =_24 new SimpleOps[F](v)_24}_24_24object simple extends ToSimpleOps_24_24trait SimpleSyntax[F] {_24 def F: Simple[F]_24 implicit def ToSimpleOps(v: F): SimpleOps[F] =_24 new SimpleOps[F](v)(SimpleSyntax.this.F)_24}_24_24// New definition of our Simple typeclass that provides an instance of_24// SimpleSyntax_24trait Simple[F] { self =>_24 def simplify(f: F): String_24 val simpleSyntax = new SimpleSyntax[F] { def F = Simple.this }_24}
Here we've provided two syntax operators as aliases to
simplify
(because no type class is legit without unicode
operator aliases).
_16import simple.__16_161 ⬈_16//=> Simplified Int with value of 1_16_16new C("I am C") ⬈_16//=> Simplified: C[name=I am C]_16_16List(1,2,3) ⬈_16//=> Simplified Int with value of 1_16//=> Simplified Int with value of 2_16//=> Simplified Int with value of 3_16_16// boring_16new C("bar") /^_16//=> 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)](http://tpolecat.github.io/2013/10/12/type class.html)
- 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.