So I have seen questions that ask how do you do Object Oriented Programming in Haskell, like this for example. To which the answer is along the lines of "type classes are like interfaces but not quite". In particular a type class doesn't allow a list to be built of all those types. E.g. we can't do map show [1, 1.4, "hello"]
despite that having a logical result.
Given some time I wondered if it wasn't possible to do better. So I had an attempt at coding polymorphism for a simple Shape class, which can be found below (if you like sanity probably better to stop reading now, and apologies for it being so long).
module Shapes (
Shape(..)
, Point
, Circle(..)
, Triangle(..)
, Square(..)
, location
, area
) where
data Point = Point {
xcoord :: Float
, ycoord :: Float
} deriving (Read, Show)
data Shape = CircleT Circle | PolygonT Polygon deriving (Read, Show)
data Circle = Circle {
cLocation :: Point
, cRadius :: Float
} deriving (Read, Show)
data Polygon = SquareT Square | TriangleT Triangle deriving (Read, Show)
data Square = Square {
sLocation :: Point
, sLength :: Float
} deriving (Read, Show)
-- only right angled triangles for ease of implementation!
data Triangle = Triangle {
tLocation :: Point
, tSide1 :: Float
, tSide2 :: Float
} deriving (Read, Show)
class ShapeIf a where
location :: a -> Point
area :: a -> Float
instance ShapeIf Shape where
location (CircleT a) = location a
location (PolygonT a) = location a
area (CircleT a) = area a
area (PolygonT a) = area a
instance ShapeIf Polygon where
location (SquareT a) = location a
location (TriangleT a) = location a
area (SquareT a) = area a
area (TriangleT a) = area a
instance ShapeIf Square where
location = sLocation
area a = (sLength a) ^ 2
instance ShapeIf Circle where
location = cLocation
area a = pi * (cRadius a) ^ 2
instance ShapeIf Triangle where
location = tLocation
area a = 0.5 * (tSide1 a) * (tSide2 a)
Despite all the madness this ends up having some quite nice properties: I can have a list of shapes and I can map functions over them that make sense (like location and area). But also if I have a particular Shape (say a Triangle) then I can also call area just on that. But it is horrendous. I don't like the code at all (indeed I'm sure it would be much shorter in any object oriented programming language).
So where have I gone wrong? How can this be made nicer? Saying "don't think in terms of objects" is nice, but this seems to have several applications (e.g. a list of characters in a role playing game ... who have some shared attributes but different abilities, or GUI programming where objects tend to make sense).
You can use simple data types for this purpose without resorting to typeclasses. If you do want to use typeclasses, it's better to use it to describe a conversion to your base type rather than having it include all the implementation details:
This might be the only two types you need, depending on your application, since you could write functions
But maybe you want to preserve those arguments. In which case, write a data type for each
Then for convenience, I'd use a typeclass here to define
toShape
:But now there's the problem that you have to convert each type to
Shape
in order to get its area or location in a more generic way, except you can just add the functionsI would keep these out of the
IsShape
class so that they can't be re-implemented, this is similar to functions likereplicateM
that work on allMonad
s, but aren't part of theMonad
typeclass. Now you can write code likeAnd this is fine when you're only operating on a single shape argument. If you want to operate on a collection of them:
So that you don't have to rely on existentials to build a collection of them you can instead have
This gives you the flexibility to work on a list of objects of different types, or on a list of just a single type using the same function and absolutely no language extensions.
Bear in mind that this is still trying to implement a sort of object model in a strictly functional language, something that isn't going to be completely ideal, but considering this allows you to have
totalArea :: IsShape s => [s] -> Float
)Shape
and add more methods to it then alias them like witharea
andlocation
and probably some other OOP paradigms, all with really less code than it would take in Java or C#, the only difference is that the code isn't all grouped together. This has it's benefits and disadvantages, such as being able to define new instances and data types more freely, but making the code somewhat more difficult to navigate.
You can get madder.
Analysed in Haskell terms, declaring a Java-style class does a number of things:
Whew. Features like interfaces, final classes, etc, basically allow you to skip parts of that list if you don't need/want the whole bundle. And on top of that, Java-style classes also provide a module system, which I'm not going to address at all.
Seen this way, you can get all of the above in Haskell if you use an "OO design pattern" to implement each of them yourself. But in a language like Java there's a lot of help that is provided by the language, which would manifest in Haskell as sensible defaults and syntactic sugar if it were present. An example is inheritance, which is basically automatic containment of superclass records within subclass records and automatic delegation to superclass implementations from subclass implementations. Haskell will give you none of this help, so everything must be explicit and the "OO design pattern" comes out incredibly verbose.
Part 1 is pretty easy to see; a set of types sharing a common interface is what a type class is. Haskell allows us to put superclass constraints on the new type class too. Done.
Part 2 is also straightforward; just declare a new data type holding all the member variables. Note that if you intend to be able to "inherit" from this "class" and use the same assessors to get member variables out, you'll want to have those as part of the type class, not just use Haskell's record syntax to declare them for you. And if you're "inheriting" from other "OO pattern" classes, you'll want to included their data types as members of your new data type.
Part 3 is where the lack of help from the language starts to get tedious. You need to implement instances for each type class implied by the OO inheritance hierarchy, going all the way up (i.e. not just the immediate bases). If you're not overriding "methods" then this will be extremely mechanical and tedious, because you can just delegate all the "inherited" methods to the contained member data of the base classes (which should already have all the needed instances if you're following the pattern). This is manually implementing what OO inheritance defaults for you.
Part 4 is the doozy. OO programmers are masters of existentially quantified types, they just don't know it. Haskell supports existentially quantified types, but only through extensions, and a little awkwardly. And the language, idioms, and libraries aren't really expecting you to make really heavy use of existential types, so you'll start to experience a lot of friction using them; mostly in the form of annoying type errors that go away when you manage to figure out the correct type to write explicitly, and occasionally you'll need to explicitly eta expand (i.e. turn
f = foo
intof x = foo x
, where the logic of higher order functions should say that makes no difference).You might think that we shouldn't need the existential types, since type-class-constrained type variables should be enough to allow code to work on any member of the type class. The trouble is that a type variable constrained by a type class must be instantiated at each call to any one type in the type class (and the choice is made by the caller, not by whatever data happens to arrive at runtime).
This is why type classes don't allow you to use heterogenous lists; although the type
Shape a => [a]
can hold objects of any type that implements Shape, there is only one single type variable for all the elements of the list, so they all must be the same "any type that implements Shape". An existential type is a wrapper that contains data with a type variable but where the wrapper does not itself have that type variable in its own type. This allows you to just have a list of[Shape]
, where it'sShape
that internally contains aShapeI a => a
.I think I've exhausted how well I can explain this without example code, so here goes. Warning, it's pretty ugly:
Given all that:
So you see we do get heterogenous lists (or in general, types which can independently hold any member of a "class" and allow access to that class' common interface), OO style inheritance hierarchies (though with manual boilerplate to inherit methods unchanged), and even upcasting.
Another issue you'll likely run into is how strict Haskell is about type discipline. You won't be able to downcast; in fact you won't be able to refer to any property of a
Shape
existential other than what's implied by theShapeI
interface; all knowledge about what it particularly contains is gone.This also means that the
shapes
list is close to useless; the only meaningful thing we can do with it ismap (apply area) shapes
, so we might as well have done away with the masses of boilerplate and just created a list ofDouble
in the first place. Also, the root class in OO languages tends to provide a surprising amount of functionality; you cantoString
arbitrary objects in Java, compare them for equality, etc. You get none of that here. Once something's an existential reference, you can access nothing but what its constraints say you can. NoShow
constraint, noshow
method (even though all of the types I've used here do implementShow
). Likewise, noEq
constraint, no==
function; and that probably wouldn't work as you'd like here because (being an idiomatic Haskell function and not expecting to deal with existentials emulating OO class heirarchies)==
only works on two values guaranteed to be of the same type, and the existential reference gives up all knowledge about being of any particular type so you could never guarantee that.I'm certain you could refine the pattern above to make it more usable, maybe even automate bits of it (can we write a generic
upcast
function? can TemplateHaskell generate the boilerplate for us?). If you threw in constraints likeTypeable
into the mix you should even be able to get runtime-checked downcasts if you really want, and might be able to implement an equality operator that worked (returningFalse
for different concrete types and delegating to==
when the types do match). But personally I'm not terribly inclined to try to flesh this out further.TLDR: OO style classes (ignoring mutations) are basically a particular combination of type classes, types holding member data, existential types, with a whole lot of default machinery to make it work easily instead of being a huge pain. Given that Haskell gives you each of those pieces as orthogonal minimal concepts, I find it much easier to know and understand those concepts separately and apply them individually or in concert as they are needed, rather than to take OO's swiss-army-knife approach and try to force every program to fit the facilities provided by that structure.
After being pointed to this blog post about existential quantification being an anti-pattern (which I had reinvented in a slightly clumsier way), I had a try at a rewrite and came up with:
You can use existential quantification for such purposes:
But note, that there are no downcasts in Haskell, so it's not a direct analogue to Java-style subtyping. Have a look at this also.