Type classes explained
Polymorphism is probably the most important feature in high level languages. It allows us to build programs according to interfaces, operate on abstractions and choose the right implementation based on types. Different languages implement it differently. Most OOP languages usually use inheritance and some kind of run time type dispatch or table lookup to get the right implementation. There is another way, which originally comes from Haskell which involves “type classes”. In this article we’ll go through the process of transforming traditional OOP style program into equivalent program with type classes in order to understand where the idea comes from.
Polymorphism via inheritance
Let’s start with a classic OOP example — two dimensional shapes representation. We have a notion of a shape that has some area and multiple implementations for each kind of shape:
Take a look at how we use our custom implementations (lines 20 and 21). We’re creating an instance of a class (which also contains implementation) and explicitly passing it as a parameter to our generic function which works on
Shapes. Now let’s take that example and try to do the same using type classes. We will go through a number of transformations making small changes to our program to eventually come up with the solution. Not every step will make sense but I will try to explain the thought process behind it.
Polymorphism via type classes
OOP approach consolidates data and related functions in one place— class definition. “Type classes” go with a different approach — entities representing data are decoupled from entities responsible for implementation.
It will sound strange and confusing at first. But no worries, it will make sense shortly.
As a first step, we will not directly extend our
Shape but introduce a new class to do that:
We have our
Rectangle case classes while the fact that they can have an area is represented with
RectangleShape. This is important, this is where the idea of separating data from functionality comes from.
Now, it doesn’t seem like we achieved anything, even worse, we have two problems now:
- Code duplication — if we want to change
Circlewe would also need to change
- To call
areaOfwe need to pass instance of
CircleShape, not the
Lets try to fix the first problem:
RectangleShape don’t have any constructor parameters, thus no code duplication, great. The problem is — we needed them to calculate the area.
CircleShape has to have information about
Circle, but not in the constructor parameters. It feels like our
area function is missing something. Imagine if instead of taking no arguments it would take a data structure representing the shape —
Rectangle case classes:
Good, now we have information to calculate the area. But how do you actually call
area? And how should
area declaration look like? We want the argument to be of type
Rectangle but they don’t share any common ancestor (which was the point of separating them). Turns out we can do it easily, just have to introduce a type parameter:
The key here is to extend parameterized
Shape with the type we want our
area to get the argument of. But it’s still not clear how to implement
areaOf and thread actual
shape.area(). Well, we don’t even pass any information about the shape we want to get the area for. Clearly we need to change
areaOf and add second parameter with actual shape info:
Cool, but let me just rename the arguments to make more sense:
shape is our shape definition (
shapeImpl is an implementation of a
Shape interface for that particular definition.
Its all good but we actually cheated a little bit by changing our
areaOf function. The calls from the very first OOP example looked like
areaOf(new Circle(10)). Now we have to pass some weird implementation object as an additional parameter. We know that we do need that implementation object but we don’t want to pass it explicitly. Is that possible?
The answer is yes, moreover, scala has this as a language feature called implicit parameters. (Check out this this article explaining implicits)
Lets modify our
shapeImpl will be passed as an implicit parameter:
areaOf takes a single parameter now but the code won’t compile. We need implicit variables to be in scope:
It feels like a boilerplate, we have to instantiate them or import to the scope each time we want to call
areaOf. Can we instantiate them only once and not think about having them in scope? Yes, implicit objects to the rescue:
That’s it! Implementation objects are singletons instantiated once and available for the compiler.
Hope it all made sense and if not — no worries, the concept is not easy to understand at first. Get your hands on, create your own type classes and check out some good articles, like this, this and this to understand the concept better.