Blog
Type Classes in Scala
Type classes in Scala provide ad-hoc polymorphism without inheritance. Learn the pattern using implicits and understand when type classes beat traditional OOP.
Type classes provide ad-hoc inheritance which means that we can use them to create polymorphic functions that can be applied to arguments of different types. This is a fancy way of saying that we can create common behaviour for classes without resorting to traditional (extends) polymorphism.
From the Neophytes Guide, Daniel Westheide describes type classes, slightly paraphrased, as follows.
A type class
Cdefines behaviour.
TypeTmust support behaviour defined inCto be a “member” ofC.
IfTis a “member”, it isn”t inherent to that type (ifThasC’s behaviour, it isn’t native to that type viaextendsor otherwise).
Instead, anyone can supply implementations ofCbehaviour for typeTand this infers thatTis a “member” ofC.
How to Create Type Classes
- Define behaviour
Cas a trait - Provide default implementations for your types (e.g.
Tabove) - Call the behaviours of
Cin a common way (optionally extending “members” likeTwith implicit classes)
Example from the Neophytes Guide
The type class here is
NumberLikeproviding abstractplus,divideandminusbehaviours.
TypesIntandDoubleare “members” ofNumberLike.
IntandDoubledon’t natively have the behaviors ofNumberLike.
Instead, the implementations on theNumberLikeobject provides them.
Step 1: Define Behaviour (as a trait)
Notice the paramaterised type [T].
object Example {
trait NumberLike[T] {
def plus(x: T, y: T): T
def divide(x: T, y: Int): T
def minus(x: T, y: T): T
}
}
Step 2: Provide Implementations
Provide some default implementations of your type class trait in its companion object. Usually, these are singletons (object) but could be vals. They are always implicit.
object NumberLike {
implicit object NumberLikeDouble extends NumberLike[Double] {
def plus(x: Double, y: Double): Double = x + y
def divide(x: Double, y: Int): Double = x / y
def minus(x: Double, y: Double): Double = x - y
}
implicit object NumberLikeInt extends NumberLike[Int] {
def plus(x: Int, y: Int): Int = x + y
def divide(x: Int, y: Int): Int = x / y
def minus(x: Int, y: Int): Int = x - y
}
}
Step 3a. Call the Type Class
The whole point of the pattern is to be able to provide common behaviour to classes without tight coupling or even by modifying them at all. So far, we’ve created specific behaviours for our classes (like plus above) conforming to our “contract” type class C.
To call that behaviour, we use Scala’s implicit semantics to find an appropriate implementation. It binds a concrete type of T (let’s say Int) with it’s corresponding type class (NumberLikeInt). It means we only need one method for all number-like types.
object Statistics {
def mean[T](numbers: Seq[T])(implicit number: NumberLike[T]): T = {
number.divide(numbers.reduce(number.plus), numbers.size)
}
}
So, if an implicit parameter can be found for a given type, Scala will use that implementation. The NumberLikeInt is used below.
scala> println(Statistics.mean(List[Int](1, 2, 3, 6, 8)))
4
Without an implicit in scope, you’d get an error
Error:(42, 26) could not find implicit value for parameter number: NumberLike[Int]
println(Statistics.mean(Seq(1, 2, 3, 6, 8)))
Context Bounds
Another way of writing the generic method is to use context bounds (ie, use T: NumberLike).
object Statistics {
def mean[T: NumberLike](numbers: Seq[T]) = {
val number = implicitly[NumberLike[T]]
number.divide(numbers.reduce(number.plus), numbers.size)
}
}
Step 3b. Call the Type Class (with an Implicit Class)
As a simple extension, you can extend “member” types directly using an implicit class. For example, we can add the mean method to any sequence of NumberLikes.
implicit class SeqNumberOps[T](numbers: Seq[T]) {
def mean(implicit number: NumberLike[T]): T = {
number.divide(numbers.reduce(number.plus), numbers.size)
}
}
and call mean directly.
import NumberOps
val numbers = List[Int](1, 5, 32, 43, 4)
println(numbers.mean)
or like this for Double.
val numbers = List[Double](3.2, 4.2, 3.0, 4.4)
println(numbers.mean)
Another Example
Step 1: Define Behaviour
A basic “decoder” interface that uses an Either to return a result as either successful or unsuccessful.
trait StringDecoder[A] {
def fromString(string: String): Either[String, A]
}
Step 2: Provide Implementations
We could provide an implementation to decode a string to a valid Colour type. Unsupported colours produce a “left” result.
implicit val colourTypeStringDecoder = new StringDecoder[Colour] {
def fromString(value: String) = {
val colours = List("red", "green", "yellow")
if (colours.contains(value)) Right(Colour(value))
else Left(s"$value is not a valid colour, chose one of ${colours.mkString(", ")}")
}
}
Step 3. Call the Type Classes
With an implicit class extending String, any string value can be decoded to a type A.
object StringSyntax {
implicit class StringDecoderOps(value: String) {
def decodeTo[A](implicit decoder: StringDecoder[A]) = {
decoder.fromString(value)
}
}
}
Then anywhere you have a string and you want to decode it, just go ahead.
"red".decodeTo[Colour] // right
"square".decodeTo[Colour] // Left(square is not a valid colour, chose one of red, green, yellow)