Object Oriented Programming in Haskell

Does Haskell support object oriented programming

How do you separate declaration and implementation in Haskell?

In Haskell you can define a typeclass, which is rather different from an object oriented class so don't let the name fool you. Using the keyword class, you can declare function names and type signatures which can be instantiated (implemented) elsewhere for a particular data type.

For example, the Hashable typeclass defines the hash function, which can turn any instantiated data type into an Int. Have a new, funky data type you want to be able to hash? Fine, make an instance of Hashable. The most common data types are instantiated by the module that defines Hashable (see the linked documentation for 'Instances').

Typeclasses aren't the only way to define an interface. A method that is often under-rated is a plain old data structure. Because Haskell has first class functions, you can define a data structure that has functions as fields:

data ShuttleInterface =
SI { launch :: Delay -> IO Handle
, deploy :: Payload -> IO ()
, getStatus :: IO Status
}

And your functions can build or consume this data structure:

deployAllSensors :: ShuttleInterface -> IO ()
deployAllSensors shuttle = do
status <- getStatus shuttle
let notDeployed = filter (not . deployed) (sensors status)
when (isOrbiting status) (mapM_ deploySensor notDeployed)

-- we used the well-known Haskell functions: filter, not, , when, mapM_
-- and some supporting functions were assumed:
isOrbitting :: Status -> Bool
deploySensor :: Sensor -> IO ()
sensors :: Status -> [Sensor]
deployed :: Sensor -> Bool

How do you restrict access to data in Haskell?

To provide abstraction, Haskell uses Algebraic Data Types. To protect fields developers declare a data type but don't export it's constructors - instead they only export a set of safe primitives that maintain desired invariants.

For example, the Map module provides a balanced tree. It couldn't guarantee balance if anyone could just declare a Map using the primitives of Branch and Leaf, so the makers didn't export those. Construction of a map must rely on what is exported from Data.Map (and those have access to/use the constructors by virtue of being in the same module) such as fromList, empty, singleton, and a whole bunch of modifiers.

Object oriented programming in Haskell

You just don't want to do that, don't even start. OO sure has its merits, but “classic examples” like your C++ one are almost always contrived structures designed to hammer the paradigm into undergraduate students' brains so they won't start complaining about how stupid the languages are they're supposed to use.

The idea seems basically modelling “real-world objects” by objects in your programming language. Which can be a good approach for actual programming problems, but it only makes sense if you can in fact draw an analogy between how you'd use the real-world object and how the OO objects are handled inside the program.

Which is just ridiculous for such animal examples. If anything, the methods would have to be stuff like “feed”, “milk”, “slaughter”... but “transport” is a misnomer, I'd take that to actually move the animal, which would rather be a method of the environment the animal lives in, and basically makes only sense as part of a visitor pattern.

describe, type and what you call transport are, on the other hand, much simpler. These are basically type-dependent constants or simple pure functions. Only OO paranoia ratifies making them class methods.

Any thing along the lines of this animal stuff, where there's basically only data, becomes way simpler if you don't try do force it into something OO-like but just stay with (usefully typed) data in Haskell.

So as this example obviously doesn't bring us any further let's consider something where OOP does make sense. Widget toolkits come to the mind. Something like

class Widget;

class Container : public Widget {
std::vector<std::unique_ptr<Widget>> children;
public:
// getters ...
};
class Paned : public Container { public:
Rectangle childBoundaries(int) const;
};
class ReEquipable : public Container { public:
void pushNewChild(std::unique_ptr<Widget>&&);
void popChild(int);
};
class HJuxtaposition: public Paned, public ReEquipable { ... };

Why OO makes sense here? First, it readily allows us to store a heterogeneous collection of widgets. That's actually not easy to achieve in Haskell, but before trying it, you might ask yourself if you really need it. For certain containers, it's perhaps not so desirable to allow this, after all. In Haskell, parametric polymorphism is very nice to use. For any given type of widget, we observe the functionality of Container pretty much reduces to a simple list. So why not just use a list, wherever you require a Container?

Of course, in this example, you'll probably find you do need heterogeneous containers; the most direct way to obtain them is {-# LANGUAGE ExistentialQuantification #-}:

data GenericWidget = GenericWidget { forall w . Widget w => getGenericWidget :: w }

In this case Widget would be a type class (might be a rather literal translation of the abstract class Widget). In Haskell this is rather a last-resort thing to do, but might be right here.

Paned is more of an interface. We might use another type class here, basically transliterating the C++ one:

class Paned c where
childBoundaries :: c -> Int -> Maybe Rectangle

ReEquipable is more difficult, because its methods actually mutate the container. That is obviously problematic in Haskell. But again you might find it's not necessary: if you've substituted the Container class by plain lists, you might be able to do the updates as pure-functional updates.

Probably though, this would be too inefficient for the task at hand. Fully discussing ways to do mutable updates efficiently would be too much for the scope of this answer, but such ways exists, e.g. using lenses.

Summary

OO doesn't translate too well to Haskell. There isn't one simple generic isomorphism, only multiple approximations amongst which to choose requires experience. As often as possible, you should avoid approaching the problem from an OO angle alltogether and think about data, functions, monad layers instead. It turns out this gets you very far in Haskell. Only in a few applications, OO is so natural that it's worth pressing it into the language.


Sorry, this subject always drives me into strong-opinion rant mode...

These paranoia are partly motivated by the troubles of mutability, which don't arise in Haskell.

Functional object-oriented programming in Haskell

As far as I can see, your code works fine in an untyped setting (say, Scheme or JavaScript).

In a typed setting it could work, but only if it involves fairly complex types, namely rank-2 types. The type inference engine can not infer those, which must be annotated manually.

To stress the point, let's try to use rank-1 types, only, adding all the annotations. This part compiles fine.

type Robot a = ((String, Int, Int) -> a) -> a

robot :: (String, Int, Int) -> Robot a
robot (name, attack, hp) = \message -> message (name, attack, hp)

name :: (String, Int, Int) -> String
name (nm, _, _) = nm
attack :: (String, Int, Int) -> Int
attack (_, a, _) = a
hp :: (String, Int, Int) -> Int
hp (_, _, p) = p

getName :: Robot String -> String
getName r = r name
getAttack :: Robot Int -> Int
getAttack r = r attack
getHP :: Robot Int -> Int
getHP r = r hp

setName :: Robot (Robot a) -> String -> Robot a
setName r nm = r $ \(_, a, hp) -> robot (nm, a, hp)
setAttack :: Robot (Robot a) -> Int -> Robot a
setAttack r a = r $ \(nm, _, hp) -> robot (nm, a, hp)
setHP :: Robot (Robot a) -> Int -> Robot a
setHP r hp = r $ \(nm, a, _) -> robot (nm, a, hp)

printRobot :: Robot String -> String
printRobot r = r $ \(nm, a, hp) -> nm ++ " attack:" ++ show a ++ " hp:" ++ show hp

damage :: Robot (Robot a) -> Int -> Robot a
damage r ad = r $ \(nm, a, hp) -> robot (nm, a, hp - ad)

fight :: Robot Int -> Robot (Robot a) -> Robot a
fight atacker defender = damage defender power where
power = if getHP atacker > 10
then getAttack atacker
else 0

Above, a Robot a indicates a robot value which can only be used to compute values of type a. E.g. from a Robot Int you can extract the attack and HP, but not the name.

Looking at the code... a lot of weird types arise! The type of fight is very puzzling:

fight :: Robot Int -> Robot (Robot a) -> Robot a

The first robot must produce its attack, so it is a Robot Int, while the second one must fight and produce a Robot a, hence the weird type Robot (Robot a).

From this, we get that we can't hope to type both fight r1 r2 and fight r2 r1: that would require Int = Robot a, which is impossible.

  • Couldn't match type ‘Int’ with ‘((String, Int, Int) -> a) -> a’
Expected type: Robot (Robot a)
Actual type: Robot Int

What could be the solution? Use rank-2 robots:

newtype Robot = Robot (forall a. ((String, Int, Int) -> a) -> a)

The forall a here indicates that a rank-2 robot can generate any result we choose, not just a single one. Hence, from a rank-2 robot we can extract both name and HP.

We need to wrap/unwrap everything using the constructor, which can be a bit annoying:

robot :: (String, Int, Int) -> Robot
robot (name, attack, hp) = Robot (\message -> message (name, attack, hp))
getName :: Robot -> String
getName (Robot r) = r name
-- etc.

Now, fight should work. I'll leave the rest for the OP to try.

Note that theoretical results (Yoneda's lemma) state that the polymorphic type we used, forall a. ((String, Int, Int) -> a) -> a is isomorphic to (String, Int, Int), so we indeed we have reinvented tuples in a more complicated way.

Concluding: I am a bit surprised that a Haskell book suggested this approach. It seems quite advanced material to me. I wonder what the intended solution was.

Object-oriented programming in a purely functional programming context?

The disconnect you see is not of FP vs. OOP. It's mostly about immutability and mathematical formalisms vs. mutability and informal approaches.

First, let's dispense with the mutability issue: you can have FP with mutability and OOP with immutability just fine. Even more-functional-than-thou Haskell lets you play with mutable data all you want, you just have to be explicit about what is mutable and the order in which things happen; and efficiency concerns aside, almost any mutable object could construct and return a new, "updated" instance instead of changing its own internal state.

The bigger issue here is mathematical formalisms, in particular heavy use of algebraic data types in a language little removed from lambda calculus. You've tagged this with Haskell and F#, but realize that's only half of the functional programming universe; the Lisp family has a very different, much more freewheeling character compared to ML-style languages. Most OO systems in wide use today are very informal in nature--formalisms do exist for OO but they're not called out explicitly the way FP formalisms are in ML-style languages.

Many of the apparent conflicts simply disappear if you remove the formalism mismatch. Want to build a flexible, dynamic, ad-hoc OO system on top of a Lisp? Go ahead, it'll work just fine. Want to add a formalized, immutable OO system to an ML-style language? No problem, just don't expect it to play nicely with .NET or Java.


Now, you may be wondering, what is an appropriate formalism for OOP? Well, here's the punch line: In many ways, it's more function-centric than ML-style FP! I'll refer back to one of my favorite papers for what seems to be the key distinction: structured data like algebraic data types in ML-style languages provide a concrete representation of the data and the ability to define operations on it; objects provide a black-box abstraction over behavior and the ability to easily replace components.

There's a duality here that goes deeper than just FP vs. OOP: It's closely related to what some programming language theorists call the Expression Problem: With concrete data, you can easily add new operations that work with it, but changing the data's structure is more difficult. With objects you can easily add new data (e.g., new subclasses) but adding new operations is difficult (think adding a new abstract method to a base class with many descendants).

The reason why I say that OOP is more function-centric is that functions themselves represent a form of behavioral abstraction. In fact, you can simulate OO-style structure in something like Haskell by using records holding a bunch of functions as objects, letting the record type be an "interface" or "abstract base class" of sorts, and having functions that create records replace class constructors. So in that sense, OO languages use higher-order functions far, far more often than, say, Haskell would.

For an example of something like this type of design actually put to very nice use in Haskell, read the source for the graphics-drawingcombinators package, in particular the way that it uses an opaque record type containing functions and combines things only in terms of their behavior.


EDIT: A few final things I forgot to mention above.

If OO indeed makes extensive use of higher-order functions, it might at first seem that it should fit very naturally into a functional language such as Haskell. Unfortunately this isn't quite the case. It is true that objects as I described them (cf. the paper mentioned in the LtU link) fit just fine. in fact, the result is a more pure OO style than most OO languages, because "private members" are represented by values hidden by the closure used to construct the "object" and are inaccessible to anything other than the one specific instance itself. You don't get much more private than that!

What doesn't work very well in Haskell is subtyping. And, although I think inheritance and subtyping are all too often misused in OO languages, some form of subtyping is quite useful for being able to combine objects in flexible ways. Haskell lacks an inherent notion of subtyping, and hand-rolled replacements tend to be exceedingly clumsy to work with.

As an aside, most OO languages with static type systems make a complete hash of subtyping as well by being too lax with substitutability and not providing proper support for variance in method signatures. In fact, I think the only full-blown OO language that hasn't screwed it up completely, at least that I know of, is Scala (F# seemed to make too many concessions to .NET, though at least I don't think it makes any new mistakes). I have limited experience with many such languages, though, so I could definitely be wrong here.

On a Haskell-specific note, its "type classes" often look tempting to OO programmers, to which I say: Don't go there. Trying to implement OOP that way will only end in tears. Think of type classes as a replacement for overloaded functions/operators, not OOP.

As OOAD is to OOP what is the equivalent for functional programming?

According to Simon Peyton Jones:

The language in which you write profoundly affects the design of
programs written in that language. For example, in the OO world, many
people use UML to sketch a design. In Haskell or ML, one writes type
signatures instead. Much of the initial design phase of a functional
program consists of writing type definitions. Unlike UML, though, all
this design is incorporated in the final product, and is
machine-checked throughout.

Source: Masterminds of Programming

So instead of drawing all the fancy UML diagrams, you actually write type definitions coupled with undefined in the design phase.

A simple object-oriented-class 'Point' in Haskell

First off: indeed, you should try not to "think OOP" in Haskell!

But your code isn't really OOP at all. It would be OO if you started to try virtual inheritance etc., but in this example it's more that the OO implementation happens to resemble the obvious Haskell implementation.

Only, it should be emphasised that the type class PointFamily really doesn't have any particular 1:1 relation with the data type Point2D, like their bundling in the OO class. You would in practise make instances of this class for any type where it conceivably works. Unsurprisingly, all of this has been done before; the most widespread equivalent of PointFamily is AffineSpace from the vector-spaces package. That is a lot more general, but has in principle much the same purpose.



Related Topics



Leave a reply



Submit