A Simple type class

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.


_10
class C(val name: String)
_10
_10
implicit 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]:


_10
def notEqual[A: Equal](a1: A, a2: A): Boolean =
_10
!implicitly[Equal[A]].equal(a1, a2)
_10
_10
val c1 = new C("foo")
_10
val c2 = new C("bar")
_10
_10
notEqual(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:


_10
def notEqual[A](a1: A, a2: A)(implicit e: Equal[A]) = !e.equal(a1, a2)
_10
notEqual(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:


_10
def notEqual[A: Equal](a1: A, a2: A): Boolean = a1 /== a2
_10
// or
_10
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.


_10
implicit 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.


_10
new 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.


_10
trait 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.


_10
def 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:


_10
implicit 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:


_10
implicitly[Simple[C]].simplify(new C("hello"))

Or we can try out the manySimple method:


_10
manySimple(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.


_10
implicit 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?


_10
implicitly[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]
_10
implicit 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:


_10
implicitly[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:


_24
final 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
_24
trait ToSimpleOps {
_24
implicit def ToSimpleOps[F](v: F)(implicit F0: Simple[F]) =
_24
new SimpleOps[F](v)
_24
}
_24
_24
object simple extends ToSimpleOps
_24
_24
trait 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
_24
trait 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).


_16
import simple._
_16
_16
1 ⬈
_16
//=> Simplified Int with value of 1
_16
_16
new C("I am C") ⬈
_16
//=> Simplified: C[name=I am C]
_16
_16
List(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
_16
new 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.