5 ms·
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,
by bad_user 9y ago
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.
- tathougies 9y agoAll 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++.
- leshow 9y ago> This is called "constrained parametric polymorphism" The more common terminology is ad-hoc polymorphism AFAIK.
- Tarean 9y ago> 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.
- deleted 9y ago[deleted]
- rocqua 9y agoTechnically, 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.
- bad_user 9y ago"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.
- tromp 9y agoThis is a single Show instance that pattern matches to decide on whether to recurse, same as how lists are shown.
- Klockan 9y ago> 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.
- bad_user 9y agoI 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.
- Klockan 9y agoIt 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 https://pastebin.com/DiFh4GuM
- smelterdemon 9y ago>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.
- gpderetta 9y agoit 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).