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
C
defines behaviour.
TypeT
must support behaviour defined inC
to be a “member” ofC
.
IfT
is a “member”, it isn’t inherent to that type (ifT
hasC
’s behaviour, it isn’t native to that type viaextends
or otherwise).
Instead, anyone can supply implementations ofC
behaviour for typeT
and this infers thatT
is a “member” ofC
.
How to Create Type Classes
- Define behaviour
C
as a trait - Provide default implementations for your types (e.g.
T
above) - Call the behaviours of
C
in a common way (optionally extending “members” likeT
with implicit classes)
Example from the Neophytes Guide
The type class here is
NumberLike
providing abstractplus
,divide
andminus
behaviours.
TypesInt
andDouble
are “members” ofNumberLike
.
Int
andDouble
don’t natively have the behaviors ofNumberLike
.
Instead, the implementations on theNumberLike
object 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 val
s. 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 NumberLike
s.
|
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.
|