Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

It may even be fair to say that Haskell's typeclasses make it more like an object-oriented programming language. They have things like properties and multiple inheritance. Even Wadler has a section leading with "Object-oriented programming." in his paper on parametric polymorphism:

https://people.csail.mit.edu/dnj/teaching/6898/papers/wadler... (page 3)



Polymorphism — If you understand by OOP the single-dispatching made at runtime, then type classes are different because type classes are solved at compile time, the "vtable" (the dictionary of functions) being passed around isn't virtual and is provided per type, not per instance.

Encapsulation — If you understand by OOP the encapsulation of data, then no, type classes have no such thing.

Inheritance — type classes don't have inheritance. What they have are constraints (e.g. you can implement typeclass X for type T if a typeclass implementation Y also exists for T). So you can say for example that MonadError can be implemented only for types already implementing Monad. That's not inheritance, but composition.

Most importantly perhaps and something that doesn't clearly follow from the above is that OOP as implemented in popular languages, including C++, gives you subtyping, whereas type classes do not.

This has important ramifications. For example downcasting isn't possible. And you no longer have the "liskov substitution principle". And subtyping is actually incompatible with Hindley-Milner type inference.

Also in actual usage the differences can be quite big — for example, if you view type classes as "restrictions" on what a type can do (like OOP interfaces are), you no longer need to add these restrictions on the types themselves, but on the operations, where you actually need them, while the data structure definition can remain free of such restrictions.

This is called "constrained parametric polymorphism" btw. Which can't be achieved in most OOP languages that I know of, except for Scala and that's because Scala has "implicit parameters" which are equivalent to Haskell's type classes.

At the end of the day, what a type class gives you is a way to transform a type name into a meaningful value. Imagine having a function like ...

     f: T => A
But the T param is a type name. That's what type classes give you, whereas OOP does not.


> Which can't be achieved in most OOP languages that I know of, except for Scala and that's because Scala has "implicit parameters" which are equivalent to Haskell's type classes.

C++ can do that with template specialization.


I didn't know that, but it doesn't surprise me, seems to me like C++ templates are like their own language grown within C++ and can do lots of crazy things.


It is fairly straightforward, created a pastebin showing how it can be done, you can also make it check that it is implemented at compile time but the logic around that is a bit iffy:

https://pastebin.com/DiFh4GuM


> type classes are different because type classes are solved at compile time

    data Nested a = Nest a (Nested [a]) | Done deriving Show

    build :: Int -> a -> Nested a
    build 0 a = Done
    build i a = Nest a $ build (i- 1) [a]

    main = do
        i <- readLn
        print (build i i)



if you build a value of Nested at runtime then it is impossible to do static dispatch and the dictionary has to be constructed at runtime as well.

Sorry that I mistyped the example at first. If you run this version:

    > main
    23
    Nest 23 (Nest [23] (Nest [[23]] (Nest [[[23]]] (Nest [[[[23]]]] (Nest [[[[[23]]]]] (Nest [[[[[[23]]]]]] (Nest [[[[[[[23]]]]]]] (Nest [[[[[[[[23]]]]]]]] (Nest [[[[[[[[[23]]]]]]]]] (Nest [[[[[[[[[[23]]]]]]]]]] (Nest [[[[[[[[[[[23]]]]]]]]]]] (Nest [[[[[[[[[[[[23]]]]]]]]]]]] (Nest [[[[[[[[[[[[[23]]]]]]]]]]]]] (Nest [[[[[[[[[[[[[[23]]]]]]]]]]]]]] (Nest [[[[[[[[[[[[[[[23]]]]]]]]]]]]]]] (Nest [[[[[[[[[[[[[[[[23]]]]]]]]]]]]]]]] (Nest [[[[[[[[[[[[[[[[[23]]]]]]]]]]]]]]]]] (Nest [[[[[[[[[[[[[[[[[[23]]]]]]]]]]]]]]]]]] (Nest [[[[[[[[[[[[[[[[[[[23]]]]]]]]]]]]]]]]]]] (Nest [[[[[[[[[[[[[[[[[[[[23]]]]]]]]]]]]]]]]]]]] (Nest [[[[[[[[[[[[[[[[[[[[[23]]]]]]]]]]]]]]]]]]]]] (Nest [[[[[[[[[[[[[[[[[[[[[[23]]]]]]]]]]]]]]]]]]]]]] Done))))))))))))))))))))))

The innermost Nested has type `Nest [[[[[[[[[[[[[[[[[[[[[[Int]]]]]]]]]]]]]]]]]]]]]]` which wasn't known at compile time so the show instance has to be constructed at runtime. This is called polymorphic recursion.


Technically, one might argue that i doesn't store a vtable, but only a reference to a type, which stores a vtable determined at runtime.

I'd say the difference is minor to non-existent.


"i" does not store a reference to a type. Values in Haskell aren't tagged with their type like in OOP.

There is no "isInstanceOf" check in Haskell.


This is a single Show instance that pattern matches to decide on whether to recurse, same as how lists are shown.


Yeah, sorry, forgot the type variable at first.


All you need is a GADT which carries around the type class constraint.

    data Object cls a where
      Object :: cls a => a -> Object cls a
Now, the class `cls a` is carried around with the value `a`. You can even go one step further and reify the vtable by itself.

    data Dict c where
      Dict :: c => Dict
which you can't really do in standard C++.


>the "vtable" (the dictionary of functions) being passed around isn't virtual and is provided per type, not per instance.

This might be nitpicking but vtables in C++ are created per type with instances having a pointer to it.


it is not nitpicking, you are correct to point it out. The only difference is where the vtable is attached to: pointers in Haskell, objects instances in c++.

Although the language does not offer a builtin syntax for it, in C++ is also common to attach the 'vtable' to the 'handle' (a smart pointer or deep copying envelope), in the case of type erased wrappers (std::function, std::any, etc).


> This is called "constrained parametric polymorphism"

The more common terminology is ad-hoc polymorphism AFAIK.


ad-hoc polymorphism is more general. Constrained/Bounded parametric polymorphism is a controlled subset of violations of parametric polymorphism.


eh, that might be a bit of a stretch. I like my peanut-butter and jelly separate, thank you.


Rust's traits are basically typeclasses, and so people coming from an OOP background have often struggled to model stuff in a Rust-y way. The 17th chapter of The Rust Programming Language [1] tries to get into the differences, and explain this kind of stuff. It's really hard though, since what OOP means depends on who you ask.

I enjoyed this post since it's kind of a mirror of this chapter, which was one of the hardest for us to figure out what to put in it.

1: https://doc.rust-lang.org/book/second-edition/ch17-00-oop.ht...


> It may even be fair to say that Haskell's typeclasses make it more like an object-oriented programming language.

The trick here is defining "object-oriented". In modern object-oriented languages, I usually assume that involves encapsulation of state inside of boxes called objects. In that sense, type classes are quite different.

Methods on objects generally mutate or provide views into the contents of the box, while methods in type classes are just regular functions. Critically, there is no inner mutation or privileged view into data via type classes.

> They have things like properties and multiple inheritance

How do they have properties? Also, they don't have multiple inheritance - a type can only ever have one instance of any given typeclass.

> Even Wadler has a section leading with "Object-oriented programming."

I read Wadler's paragraph on object-oriented programming (bottom of page 3) more as a highlight of the fundamental _difference_ between type classes and object-oriented classes. The only similarity between typeclasses and classes are that they are the mechanisms by which runtime dispatch is achieved.


OOP has nothing to do with classes, inheritance, especially multiple inheritance. It's about programming objects that communicate with each other. In fact Javascript and it's prototype system are much more OOP than say C++ classes, or worse, Java classes.

We should call programming with classes and inheritance. COP, as in Class Oriented Programming. COP is especially appropriate because if you don't follow the type rules, you go to compiler jail.


> OOP has nothing to do with classes, inheritance, especially multiple inheritance. It's about programming objects that communicate with each other.

That is the Alan Kay definition. I'm not sure I'd claim that is the colloquial, universal understanding of OOP apart from the HN crowd, though. (ask a LOB programmer what OOP is and observe what they say).

I rather like the Alan Kay definition myself, but that is rather like having a Many Worlds understanding of quantum mechanics; Entirely consistent and arguably "correct", but not universally shared among practitioners.

We should refine our language (e.g. COP), but it would be revisionist to ignore what the 90's and 00's understanding of the definition would be.

I think it would be even more of a stretch to say that prototypal languages (javascript) are more OOP than non-prototypal languages, unless of course one is being a bit reductionist.


> In fact Javascript and it's prototype system are much more OOP than say C++ classes, or worse, Java classes.

I'm not sure what your ordering relation is here, but if it has something to do with how "purely object-oriented" a language is, then, sure, JavaScript is more OOP than C++ and Java since classes are not first-class in the latter.

But Smalltalk, which is class-based, is even more OOP than JavaScript since in Smalltalk, constructor calls are simply regular method calls (unlike JS which has a special "new" keyword) and there are no primitive objects (unlike JS, which distinguishes between primitive types and their boxed forms, though it tries to obscure that fact).

> COP is especially appropriate because if you don't follow the type rules, you go to compiler jail.

Smalltalk, Ruby, and Python are all class-based OOP languages but do not have static type systems with lots of compiler errors.


> It's about programming objects that communicate with each other.

This becomes even more apparent when using an actor-model language like Erlang. The types of "design patterns" you use focus much more around the communication and coordination you might find in real, physical systems. Consequently I find it easier to model "real" systems using actors than standard objects in a language like Java.

I also appreciate how the Pony language decomposes the notion of "objects" into actors that have their own process and capabilities (standard objects). It shows that their is a distinct role for both types of objects, and it helps when decomposing entities in a system.


That entirely depends on who you ask.

If classes and inheritance aren't a form of OOP, then what are they?

Also, Smalltalk had classes and inheritance, whatever Alan Kay had to say on the matter.


Object is a sufficiently generic term, so that it can mean very different things to different people. Lumping state with some functions that implicitly have R/W access to that state is not possible in Haskell, not even with the powerful type-classes. Thankfully.

Type-classes do allow interesting polymorphism tricks, that —as Wadler mentioned— allows Haskellers to use some of good parts of OO.


You're right; I still struggle to come up with a good catch-all definition for "object." On an unrelated note, I'm also still trying to figure out what DevOps is, aside from YAML and tears.


DevOps provisioning by software (hence the "dev") instead of by hand (manual-Ops).

It is with cloud-native that it gets interesting. Cloud-native infrastructure (like k8s) usually replaces the traditional devops tools (like Puppet, Ansible and Salt). Also it brings the notion of "scrappable instances", "blue green deployments" and "stateless compute nodes". This is were it got really interesting, this is were the FP intuitions start carry over. :)


Interesting last point! It does seem to be the trend, notably with large data sets:

https://wycd.net/posts/2017-04-09-rpcs-for-moving-computatio...


Lumping state with some functions that implicitly have R/W access to that state is not possible in Haskell

s/not possible/possible, but highly discouraged/




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: