(this post is part of a series on Object Oriented Programming in Haskell – see tutorials for a table of contents)
Does Haskell have inheritance?
Well, no, it doesn’t, because Haskell does not have objects, and inheritance is a relationship between two objects. Objects are a combination of internal state (data) and methods (behavior). Since inheritance is a combination of both of these things, we’ll need to treat them separately.
This post will approach data inheritance, since it is somewhat easier to deal with.
tl;dr: Decompose OO features into simple parts, reconsitute with FP
Let’s consider a simple example of data-only inheritance. We’ll use Java for this example.
class Shape {
public final int x;
public final int y;
public Shape(int x, int y) {
this.x = x;
this.y = y;
}
}
class Circle extends Shape {
public final int radius;
public Circle(int x, int y, int radius) {
super(x, y);
this.radius = radius;
}
}
Here we’ve defined a class Shape
that contains the X and Y coordinates where it is located.
We extend Shape
to create the Circle
class, which also has a Radius
.
Whenever we have a Circle
, or any other class that extends Shape
, we know we can access the x
and y
properties on that class.
In the introduction, I mentioned that inheritance can be broken down into simpler features. Let’s put on our “Objects and Messages” thinking caps, and think about what we mean with these constructs.
Circle c = new Circle(1, 2, 3);
We’ll read this as:
Create a new Circle object with the values 1, 2, 3.
c.x;
Generally, this is read as “access the property x
on the object c
.”
Let’s instead read it as:
Send the message
x
to the objectc
.
This gets us more in the “pure” object oriented sense of things.
So, when we send the message x
to the object c
, how does it know how to respond?
First, it’ll do a lookup on all of it’s attributes.
Circle
only has a radius
attribute.
It doesn’t have an x
attribute.
It’s not time to give up, though.
The next thing it does is delegate the message to the parent class.
Shape
does have x
defined, so we can respond with that value.
What if we do this: c.toString()
?
Well, Shape
doesn’t have toString()
on it’s list of attributes defined.
Java (and most OOP languages) have an implicit Object
class that is the superclass of all objects.
Object
does have toString()
defined, so c
delegates to Shape
, which then delegates to Object
.
Super classes, mixins, traits, etc. are all – conceptually – just an automatic form of delegation. “If I don’t know how to handle something, ask this object.”
If we were to ban the extends
keyword in Java, then we could recover the same functionality with object composition:
class Shape {
private final int x;
private final int y;
public Shape(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return this.x;
}
public int getY() {
return this.y;
}
}
class Circle {
private final Shape shape;
private final int radius;
public Circle(int x, int y, int radius) {
this.shape = new Shape(x, y);
this.radius = radius;
}
public int getX() {
return this.shape.getX();
}
public int gety() {
return this.shape.getY();
}
public int getRadius() {
return this.radius;
}
}
Oof, that’s a lot of boilerplate.
No wonder people prefer inheritance.
In Java, you’d probably want to define an interface so that you can use Shape
s polymorphically, but we’ll ignore that for now.
Now that we’ve simplified the implementation of data inheritance in Java to the core features of OOP, it’s easy to port it to Haskell.
data Shape = Shape { x :: Int, y :: Int }
data Circle = Circle { shape :: Shape, radius :: Int }
That’s it! We just compose two bits of data. That’s the easy way. However, we have some flexibility problems.
To access the Circle
’s x
parameter, we need to compose functions:
circleX = x . shape
circleY = y . shape
Ideally, we’d like to be able to the x
and y
functions work on any type that has those attributes.
Haskell’s solution for name overloading is the type class.
Here’s a new definition of our types:
data Shape = Shape
{ shapeX :: Int
, shapeY :: Int
}
data Circle = Circle
{ circleShape :: Shape
, circleRadius :: Int
}
Now, we’re prefixing the record fields with the type name. This saves the more general names for the type classes. For each field that we want to be polymorphic about, we create a type class and an instance for the classes:
class HasX a where
x :: a -> Int
instance HasX Shape where
x = shapeX
instance HasX Circle where
x = shapeX . circleShape
Now, we can use the function x
on any type that we instantiate it for.
We can even retrofit existing types:
instance HasX (Int, Int) where
x = fst
This is reminiscent of monkey patching in Ruby, but rather than being a gross hack, it’s an elegant way to extend types with new functionality.
We’ve covered immutable reading of values, but we also want to be able to update values. The idiomatic Java is presented here, with most of the boilerplate:
class Shape {
private int x;
private int y;
public Shape(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return this.x;
}
public void setX(int x) {
this.x = x;
}
}
class Circle extends Shape {
// pretend I'm not too lazy to
// write out the boilerplate
}
Now, we want to be able to translate:
Circle c = new Circle(1, 2, 3);
c.setX(4);
into idiomatic Haskell.
First, we’ll have to translate the above into using immutable objects, since Haskell’s data types are immutable. Let’s see what that looks like:
class Shape {
private final int x;
private final int y;
public Shape(int x, int y) {
this.x = x;
this.y = y;
}
public Shape setX(int x) {
return new Shape(x, this.y);
}
// etc...
}
Rather than modifying the old Shape, we return a new Shape with the field changed. Modifying a Circle is pretty similar. We’ll switch back to using object composition as well:
class Circle {
private final Shape shape;
private final int radius;
public Circle(int x, int y, int radius) {
this.shape = new Shape(x, y);
this.radius = radius;
}
public Circle setX(int x) {
return new Circle(
x,
this.shape.getY(),
this.radius
);
}
}
Alright, now we can directly translate this to Haskell:
class SetX a where
setX :: Int -> a -> a
instance SetX Shape where
setX x (Shape _ y) = Shape x y
instance SetX Circle where
setX x (Circle shape radius) =
Circle (setX x shape) radius
Now we can express a modification function:
modifyX :: (HasX a, SetX a) => (Int -> Int) -> a -> a
modifyX f a = setX (f (getX a)) a
Neat!
So, you may notice that the boilerplate involved with the polymorphic accessors and setters is pretty intense. Lenses solve this issue nicely. I won’t go into depth on how great lenses are and why learning them has made my life so much better, but I will recommend this tutorial and this lengthy blog series.
Classy lenses work especially well for this problem:
data Shape = Shape
{ _shapeX :: Int
, _shapeY :: Int
}
data Circle = Circle
{ _circleShape :: Shape
, _circleRadius :: Int
}
makeClassy ''Shape
makeClassy ''Circle
instance HasShape Circle where
shape = circleShape
which give us some pretty nice helpers:
someShape = Shape 1 2
someCircle = Circle someShape 3
moveShape = over x (+1) . over y (+1)
And we can use moveShape
on both a Shape
and a Circle
, and truly, anything that implements the HasShape
type class.
The somewhat-mechanical process that we followed to reach this point was:
I suspect that this process will work fairly well for most of the things we’ll run into on this series.