top | item 3373672

Haskell: OOP vs type classes

75 points| rbxbx | 14 years ago |haskell.org | reply

13 comments

order
[+] ekidd|14 years ago|reply
Haskell doesn't really do inheritance, unless you somehow get O'Haskell to work on a modern machine.

In place of inheritance, you have two mechanisms: algebraic data types and type classes. They work great for many programs, but neither of them is an exact fit for Java-style object-oriented programming.

An algebraic data type has a name ("Bool", in the first example below), and several constructors ("True" and "False"). Constructors can take positional and named arguments, and they can take type parameters:

    data Bool = True | False
    data Shape =
        Square { l :: Int, t :: Int, w :: Int, h :: Int }
      | Circle Int Int Int
    data Maybe a = Just a | Nothing
Once you define an algebraic data type, you can't add new constructors. But you can write functions which match against the existing constructors at runtime, giving you a form of runtime dispatch vaguely analogous to method lookup:

    boolToString :: Bool -> String
    boolToString True = "true"
    boolToString False = "false"
So algebraic data types work great if you have one abstract interface with known set of concrete subclasses. It's a really great way to think about complex data structures with multiple types of nodes.

A type class is a little bit like a C++ template protocol: It says that a given type implements a set of functions. But as with C++ templates, it's effectively resolved at compile time. (Well, the implementation is quite a bit different, but that's the rough idea.)

Now, ADTs and type classes are great. But if you find yourself using crazy hacks to recreate an OO class hierarchy in Haskell, you're probably fighting against the language. Either structure your program differently, or find a different language.

[+] dmooney1|14 years ago|reply
I agree - type overloading says templates to me rather than inheritance. C++'s tuple class lets you do some of the cool listy type things you can do in Haskell. Using typedef tuple<double, double> FloatPoint and taking it from there seems a lot more like the Haskell example and would be less code - one line each for the typedefs and a one-line function to implement <<. (Not taking anything away from Haskell, which I'm completely in love with.)
[+] ufo|14 years ago|reply
I would say the closest OO analog to ADTs is the Visitor pattern: You first fix the number of classes and then implement the different functions (visitors)

This is in contrast with the normal OO polymorphism/method where you first define set of methods and then implement the different classes.

----------

Type classes are more of a thing of their own. The template comparison is apt and I have heard Type Classes are supposedly very similar to the "Concepts" proposal that did not get in C++0x.

[+] gtani|14 years ago|reply
Well then, the expression problem. The guys who created scala and OCaml have opinions about this, no surprise (why not OOP and typeclasses?

http://stackoverflow.com/questions/2807629/handling-incremen...

(message 20) http://groups.google.com/group/scala-debate/browse_frm/threa...

http://lambda-the-ultimate.org/node/4400

---------------

and "ADT" i think real world haskell is correct in reservign this for abstract data types

http://book.realworldhaskell.org/read/defining-types-streaml...

[+] Chris_Newton|14 years ago|reply
It is unfortunate that two completely different concepts both happen to use the word "class" in their name. A related example is "return". People coming from a background in, say, C++ or Java will naturally have preconceptions about what these words mean, and will almost inevitably start by trying to apply them in Haskell world where they don't work.

It is particularly unfortunate because, with the wisdom of hindsight, I think classes as used in C++ and Java have been responsible for many problems over the years. This is fundamentally because they take two very common, very useful ideas -- modular design (separation of interface and implementation) and compound data types (struct/record/variant/etc.) -- and try to use a single tool to implement both at the same time.

Unfortunately, that creates all kinds of presumptions about how you write your functions, where the emphasis is always on one object being paramount or one data type controlling everything. For practical programming tasks, it may be more useful to model a situation using a set of related types to hold the data and a set of algorithms defined in terms of one or more of those data types (or more generally collections of one or more such types) to manipulate that data.

It's probably not an exaggeration to say that almost all of the most common modelling problems with C++/Java style OOP -- the "diamond pattern" with multiple inheritance, for example -- are due at least in part to this unwarranted emphasis on this/self/whatever you want to call it. Much of the rest is due to over-use of inheritance, which again has an underlying subclass-or-subtype ambiguity in what it represents -- and which again is perhaps emphasized by the whole one-object-to-rule-the-world philosophy even when what OOP would call containment or aggregation is a clearer way to model the situation.

Languages such as Haskell have never had this history of conflating modular design with structuring data, nor the resulting false friend of emphasizing one object/value/whatever in any given modelling context. IME, this is the big conceptual leap that is sometimes hard for those with a strongly everything-is-an-object background to make, though it's perhaps a little easier these days for anyone who has used C++ templates or Java generics. Once you make the jump, type classes and algebraic data types make a lot more sense.

[+] langsamer|14 years ago|reply
I think it is a bit silly to try to re-create OO with type classes or vice-versa. They are just two different modes of thinking and represent two different ways of architecting your program. Personally, I do find OOP a hoax, which leads to just horrendous code, but I'm sure either approach can be used to solve the problem at hand as long as you don't try to fit the round-peg in the square-hole.

For a case against OOP read : http://c2.com/cgi/wiki?ObjectOrientationIsaHoax

[+] tikhonj|14 years ago|reply
I haven't had time to read through the whole article (and I already know how type classes work :)) so I just have one little point to add.

As correctly put in the article, type classes can provide default implementations. However, one cute thing that was missed is that type classes can provide default implementations for every function, referencing each other:

    class  Eq a  where
    (==), (/=) :: a -> a -> Bool
    x /= y     =  not (x == y)
    x == y     =  not (x /= y)
This way you can implement == or /=, and you get the other for free.

For me, the one new idea that made me understand why type classes are awesome was the read function: it's polymorphic on its return type. This means that you always give it a String and it returns whatever you need. This also means that you can have polymorphic constants--maxBound is the maximum of any bounded numeric type, so you can use it as an Int or a Float and it will always have the right value.

Overall, I now think that type classes are awesome. However, I also don't think a parallel to OOP is particularly helpful--I also learned OOP well before functional programming and found it easier to understand type classes by looking at how they were used than by comparing them to interfaces or templates.

[+] rbxbx|14 years ago|reply
Speaking as someone who's brain has been broken by OOP, I found this article quite illuminating and would love to see more of the hardcore haskell/ML/FP guys weigh in.
[+] jlouis|14 years ago|reply
* The Haskell guys can use Algebraic Datatypes and type classes to achieve many of the desirable properties of OOP.

* The ML guys can use OCaml, where the O is Objective. Ocaml contains a concept of OOP built into the language already (namely structural subtyping - someone could call this typed duck-typing). The ML guys in general also have another tool for abstraction at their disposal: ML-style functors.

* Erlang programmers often use processes which are self-contained concurrently executing "objects". In Erlang a process is lightweight enough to be considered a "heavy" object in OOP so you often structure your programs around several processes messaging each other to carry out solutions to tasks.

Different languages provides different tools as a means of abstraction. It is all just about unearthing the alternative.

[+] exDM69|14 years ago|reply
I am a recovering object oriented programmer. For years I drank the OOP kool aid and I used to think in terms of classes, objects and design patterns and I sang the "everything is an object" mantra. What changed my mind was two things: type classes (in Haskell) and duck typing (in Python and JavaScript). Now my (unpopular) opinion is that object oriented programming is just a passing fad in programming, but it's one that has gone on for too long and done immense damage to the brains of programmers as well as software engineering in general.

A major theoretical issue with object oriented programming is that it's not based on any theory, but it's a ad hoc construction without any proofs or axioms that can be reasoned about. There are some loose principles like the Liskov Substitution Principle, but they're not really any kind of formal theory.

As I was writing OO code, I started noticing a pattern. The first was that a vast majority of the classes I wrote were practically a boilerplate-heavy way of doing partial application of functions. The second was that I almost never used plain vanilla inheritance (Cat extends Mammal extends Animal) but almost every time I used inheritance, it was only to build some design pattern used to fit a square peg into a round hole. The rest of the cases where I used inheritance were mostly implementing some kind of sum types or algebraic data types.

In dynamically typed languages, single dispatch method calls and inheritance adds very little to the language. JavaScript works just fine without classes or inheritance. Extending or composing objects works simply by adding or overwriting object members. Multiple dispatch methods (in e.g. Python) use the type of an object as metadata to select the correct implementation, but using type information directly is usually frowned upon as it's a bad fit with duck typing. It's way easier to just add the required metadata to the object and not rely on run-time typing information, and it's still prettier than using Visitor pattern for multiple dispatch. (Clojure's multiple dispatch is pretty sweet btw)

In statically typed languages, the problem is that most popular programming languages have a really crappy typing system. C-style typing is very limited but for some reason the typing system in most popular programming languages (Java, C#) is based on C's type system with a virtual function dispatch table bolted on. Add a little syntactic sugar and you've got OOP.

There's a whole world out there in more powerful and interesting typing systems. Haskell has a very practical typing system, with algebraic data types / sum types for "static polymorphism" (A finite amount of types, somewhat equivalent to the limits of Visitor pattern) and type classes for "dynamic polymorphism" where new types (that implement the same interface) may be added after the initial definition.

Basically any OOP pattern can be pretty easily reproduced in Haskell's typing discipline. However, that is often not the best solution since there are more powerful tools available. E.g. compare a factory pattern to Haskell's Read class.

Once I unlearned OOP, I felt liberated from a bunch of stupid restrictions and ideologies that should dictate what my code looks like. Programming is fun again.

[+] nwmcsween|14 years ago|reply
I always thought OOP vs other paradigms is simply structuring the program, with enough time and work you could make a any program in any paradigm you wish. Why not pluggable paradigms?