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 Animal
s, like I could have an array of (smart) pointers to Animal
s in C++.
The classes used by Haskell are similar to those used in other object-oriented languages such as C++ and Java. However, there are some significant differences: Haskell separates the definition of a type from the definition of the methods associated with that type.
Type Classes are a language mechanism in Haskell designed to support general overloading in a principled way. They address each of the concerns raised above. They provide concise types to describe overloaded functions, so there is no expo- nential blow-up in the number of versions of an overloaded function.
=> separates two parts of a type signature: On the left, typeclass constraints.
An instance of a class is an individual object which belongs to that class. In Haskell, the class system is (roughly speaking) a way to group similar types. (This is the reason we call them "type classes"). An instance of a class is an individual type which belongs to that class.
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
.
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.
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.
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 Animal
s 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With