Scala/Haskell: A simple example of type classes
I never really understood type classes when I was working with Scala but I recently came across a video where Dan Rosen explains them pretty well.
Since the last time I worked in Scala I’ve been playing around with Haskell where type classes are much more common - for example if we want to compare two values we need to make sure that their type extends the 'Eq' type class.
Learn Me a Haskell’s chapter on type classes defines them like so:
Typeclasses are like interfaces. A typeclass defines some behaviour (like comparing for equality, comparing for ordering, enumeration) and then types that can behave in that way are made instances of that typeclass. The behaviour of typeclasses is achieved by defining functions or just type declarations that we then implement. So when we say that a type is an instance of a typeclass, we mean that we can use the functions that the typeclass defines with that type.
In that chapter we then go on to create a 'YesNo' type class which defines Boolean semantics for all different types.
We start by defining the type class like so:
class YesNo a where
yesno :: a -> Bool
Any type which extends that type class can call this 'yesno' function and find out the truthyness of its value.
e.g.
instance YesNo Int where
yesno 0 = False
yesno _ = True
If we call that:
> yesno (0 :: Int)
False
> yesno (1 :: Int)
True
We get the expected result, but if we try to call it for a type which hasn’t defined an instance of 'YesNo':
> yesno "mark"
No instance for (YesNo [Char])
arising from a use of `yesno'
Possible fix: add an instance declaration for (YesNo [Char])
In the expression: yesno "mark"
In an equation for `it': it = yesno "mark"
In Scala we can use traits and implicits to achieve the same effect. First we define the 'YesNo' trait:
trait YesNo[A] {
def yesno(value:A) : Boolean
}
Then we define an implicit value in a companion object which creates an instance of the 'YesNo' type class for Ints:
object YesNo {
implicit val intYesNo = new YesNo[Int] {
def yesno(value:Int) =
value match { case 0 => false; case _ => true } }
}
We then need to call our 'yesno' function and the idea is that if we’ve defined a type class instance for the type we call it with it will return us a boolean value and if not then we’ll get a compilation error.
object YesNoWriter {
def write[A](value:A)(implicit conv: YesNo[A]) : Boolean = {
conv.yesno(value)
}
}
If we call that:
> YesNoWriter.write(1)
res1: Boolean = true
> YesNoWriter.write(0)
res2: Boolean = false
It works as expected, but if we try to call it with a type which wasn’t defined for the 'YesNo' type class we run into trouble:
> YesNoWriter.write("mark")
:10: error: could not find implicit value for parameter conv: YesNo[java.lang.String]
YesNoWriter.write("mark")
We can also define YesNoWriter like this by making use of context bounds:
object YesNoWriter {
def write[A:YesNo](value:A) : Boolean = {
implicitly[YesNo[A]].yesno(value)
}
}
I think this pattern is preferred when we might just be tunnelling the implicit parameter through to another method but we can still use it here and use the 'implicitly' method to get access to the implicit value.
I’m still not entirely sure about the use of implicits but in this case they provide another way to implement polymorphism without having to use inheritance.
Dan Rosen goes into much more detail about type classes and implicits and I wrote about how we were using them on a project I worked on last year in an earlier blog post if you want to learn more.
About the author
I'm currently working on short form content at ClickHouse. I publish short 5 minute videos showing how to solve data problems on YouTube @LearnDataWithMark. I previously worked on graph analytics at Neo4j, where I also co-authored the O'Reilly Graph Algorithms Book with Amy Hodler.