Object oriented programming in Haskell
Asked Answered
B

4

10

I'm trying to get an understanding of object oriented style programming in Haskell, knowing that things are going to be a bit different due to lack of mutability. I've played around with type classes, but my understanding of them is limited to them as interfaces. So I've coded up a C++ example, which is the standard diamond with a pure base and virtual inheritance. Bat inherits Flying and Mammal, and both Flying and Mammal inherit Animal.

#include <iostream>

class Animal
{
public:
    virtual std::string transport() const = 0;
    virtual std::string type() const = 0;
    std::string describe() const;
};

std::string Animal::describe() const 
    { return "I am a " + this->transport() + " " + this->type(); }

class Flying : virtual public Animal 
{
public:
    virtual std::string transport() const;
};

std::string Flying::transport() const { return "Flying"; }

class Mammal : virtual public Animal 
{
public:
    virtual std::string type() const;
};

std::string Mammal::type() const { return "Mammal"; }

class Bat : public Flying, public Mammal {};

int main() {
    Bat b;
    std::cout << b.describe() << std::endl;
    return 0;
}

Basically I'm interested in how to translate such a structure into Haskell, basically that would allow me to have a list of Animals, like I could have an array of (smart) pointers to Animals in C++.

Backwoods answered 25/11, 2013 at 3:31 Comment(5)
Are you familiar with type classes? en.wikipedia.org/wiki/Type_classRachellrachelle
As the question says, yes somewhat, however I'm unsure how to use them as partially implemented classes in a hierarchy (as opposed to basically a Java interface). I thought example code would be the quickest way to learn.Backwoods
@Backwoods What is your use case for this? I think you'll run into problems when you start trying to write Haskell code like you would object oriented.Sewing
@bheklilr: This is just an example to learn how C++ concepts do (or don't) translate to Haskell.Backwoods
The example needs a real world context where OOP would make sense in order to be translated properly translated into Haskell. Write a simple program where the OOP structure is important.Shapiro
U
50

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.

Ulland answered 25/11, 2013 at 8:46 Comment(0)
L
11

In Haskell there isn't a good method for making "trees" of inheritance. Instead, we usually do something like

data Animal = Animal ...
data Mammal = Mammal Animal ...
data Bat    = Bat Mammal ...

So we incapsulate common information. Which isn't that uncommon in OOP, "favor composition over inheritance". Next we create these interfaces, called type classes

class Named a where
  name :: a -> String

Then we'd make Animal, Mammal, and Bat instances of Named however that made sense for each of them.

From then on, we'd just write functions to the appropriate combination of type classes, we don't really care that Bat has an Animal buried inside it with a name. We just say

prettyPrint :: Named a => a -> String
prettyPrint a = "I love " ++ name a ++ "!"

and let the underlying typeclasses worry about figuring out how to handle the specific data. This let's us write safer code in many ways, for example

foo :: Top -> Top
bar :: Topped a => a -> a

With foo, we have no idea what subtype of Top is being returned, we have to do ugly, runtime based casting to figure it out. With bar, we statically guarantee that we stick to our interface, but that the underlying implementation is consistent across the function. This makes it much easier to safely compose functions that work across different interfaces for the same type.

TLDR; In Haskell, we compose treat data more compositionally, then rely on constrained parametric polymorphism to ensure safe abstraction across concrete types without sacrificing type information.

Lingulate answered 25/11, 2013 at 4:56 Comment(0)
D
2

There are many ways to implement this successfully in Haskell, but few that will "feel" much like Java. Here's one example: we'll model each type independently but provide "cast" operations which allow us to treat subtypes of Animal as an Animal

data Animal = Animal String String String
data Flying = Flying String String
data Mammal = Mammal String String

castMA :: Mammal -> Animal
castMA (Mammal transport description) = Animal transport "Mammal" description

castFA :: Flying -> Animal
castFA (Flying type description) = Animal "Flying" type description

You can then obviously make a list of Animals with no trouble. Sometimes people like to implement this via ExistentialTypes and typeclasses

class IsAnimal a where
  transport :: a -> String
  type :: a -> String
  description :: a -> String

instance IsAnimal Animal where
  transport (Animal tr _ _) = tr
  type (Animal _ t _) = t
  description (Animal _ _ d) = d

instance IsAnimal Flying where ...
instance IsAnimal Mammal where ...

data AnyAnimal = forall t. IsAnimal t => AnyAnimal t

which lets us inject Flying and Mammal directly into a list together

animals :: [AnyAnimal]
animals = [AnyAnimal flyingType, AnyAnimal mammalType]

but this is actually not much better than the original example since we've thrown away all information about each element in the list except that it has an IsAnimal instance, which, looking carefully, is completely equivalent to saying that it's just an Animal.

projectAnimal :: IsAnimal a => a -> Animal
projectAnimal a = Animal (transport a) (type a) (description a)

So we may as well have just gone with the first solution.

Diactinic answered 25/11, 2013 at 4:33 Comment(4)
"we've thrown away all information about each element in the list except that it has an IsAnimal instance" --> Well, strictly speaking, using downcasts is "wrong" even in OO languages. It is just that OO languages provide few facilities for getting stuff done the "right" way.Grassquit
The op was looking for how to make lists of animals, so there was a downcast lying about regardless.Diactinic
You can downcast in Haskell as well, using Typeable. It's rarely a good idea, though.Armageddon
Agreed—it's a rabbit hole I really didn't want to dive into. The only marginally good use I've been finding for that is the Typing Dynamic Typing stuff and I'm still trying to wrap my head around whether or not that's worth it.Diactinic
S
2

Many other answers already hint at how type classes may be interesting to you. However, I want to point out that in my experience, many times when you think that a typeclass is the solution to a problem, it's actually not. I believe this is especially true for people with an OOP background.

There's actually a very popular blog article on this, Haskell Antipattern: Existential Typeclass, you might enjoy it!

A simpler approach to your problem might be to model the interface as a plain algebraic data type, e.g.

data Animal = Animal {
    animalTransport :: String,
    animalType :: String
}

Such that your bat becomes a plain value:

flyingTransport :: String
flyingTransport = "Flying"

mammalType :: String
mammalType = "Mammal"

bat :: Animal
bat = Animal flyingTransport mammalType

With this at hand, you can define a program which describes any animal, much like your program does:

describe :: Animal -> String
describe a = "I am a " ++ animalTransport a ++ " " ++ animalType a

main :: IO ()
main = putStrLn (describe bat)

This makes it easy to have a list of Animal values and e.g. printing the description of each animal.

Sensory answered 4/5, 2016 at 10:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.