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].
|
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.
|
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.
|
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).
|
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.
|
and call mean directly.
|
or like this for Double.
|
Another Example
Step 1: Define Behaviour
A basic “decoder” interface that uses an Either to return a result as either successful or unsuccessful.
|
Step 2: Provide Implementations
We could provide an implementation to decode a string to a valid Colour type. Unsupported colours produce a “left” result.
|
Step 3. Call the Type Classes
With an implicit class extending String, any string value can be decoded to a type A.
|
Then anywhere you have a string and you want to decode it, just go ahead.
|