views:

547

answers:

4

Hi fellow stackies.

I am a long time OO programmer and a functional programming newbie. From my little exposure algebraic data types only look like a special case of inheritance to me where you only have one level hierarchy and the super class cannot be extended outside the module.

So my (potentially dumb) question is: If ADTs are just that, a special case of inheritance (again this assumption may be wrong; please correct me in that case), then why does inheritance gets all the criticism and ADTs get all the praise?

Thank you.

+18  A: 

I think that ADTs are complementary to inheritance. Both of them allow you to create extensible code, but the way the extensibility works is different:

  • ADTs make it easy to add new functionality for working with existing types
    • You can easily add new function that works with ADT, which has a fixed set of cases
    • On the other hand, adding new case requires modifying all functions
  • Inheritance makes it easy to add new types when you have fixed functionality
    • You can easily create inherited class and implement fixed set of virtual functions
    • On the other hand, adding a new virtual function requires modifying all inherited classes

Both object-oriented world and functional world developed their ways to allow the other type of extensibility. In Haskell, you can use typeclasses, in ML/OCaml, people would use dictionary of functions or maybe (?) functors to get the inhertiance-style extensibility. On the other hand, in OOP, people use the Visitor pattern, which is essentially a way to get something like ADTs.

The usual programming patterns are different in OOP and FP, so when you're programming in a functional language, you're writing the code in a way that requires the functional-style extensibility more often (and similarly in OOP). In practice, I think it is great to have a language that allows you to use both of the styles depending on the problem you're trying to solve.

Tomas Petricek
+1. so how is this different than Aspect Oriented Programming? it seems to be related. could you elaborate in your answer?
eruciform
" I think it is great to have a language that allows you to use both of the styles depending on the problem you're trying to solve." <-- Are you talking about Scala here?
one-zero-zero-one
@Cheryl: I was talking mainly about F# and Scala (because I know them). However, I think that Haskell may actually also fit into this category to some point thanks to typeclasses. They do not give you dynamic dispatch, but they allow you to "add new types" easily.
Tomas Petricek
Scala supports both styles, as does Haskell.
pelotom
@Tomas Petricek: Dynamic dispatch works just fine with type classes--GHC, for instance, bundles explicit type class dictionaries with values when it can't determine the exact type at compile time for a polymorphic expression using type class methods.
camccann
@camccann: Thanks for the clarification! I'm not as familiar with Haskell as I should be :-)
Tomas Petricek
@Tomas: You cannot, in general, use a dictionary of functions in any ML to emulate objects because the functions in the dictionary would have to have the same type. You might use a record of functions instead (but this is very rare). Also, you claimed that this applies to OCaml but it does not (this only applies to earlier MLs and Haskell). OCaml's polymorphic variants and (of course!) objects solved this problem.
Jon Harrop
Haskell typeclasses + existential quantification is actually OO-style polymorphism (dynamic dispatch). Feels strange though ...
Dario
@Dario: Can you please elaborate?
one-zero-zero-one
+7  A: 

Tomas Petricek has got the fundamentals exactly right; you might also want to look at Phil Wadler's writing on the "expression problem".

There are two other reasons some of us prefer algebraic data types over inheritance:

  • Using algebraic data types, the compiler can (and does) tell you if you have forgotten a case or if a case is redundant. This ability is especially useful when there are many more operations on things than there are kinds of thing. (E.g., many more functions than algebraic datatypes, or many more methods than OO constructors.) In an object-oriented language, if you leave a method out of a subclass, the compiler can't tell whether that's a mistake or whether you intended to inherit the superclass method unchanged.

  • This one is more subjective: many people have noted that if inheritance is used properly and aggressively, the implementation of an algorithm can easily be smeared out over a half a dozen classes, and even with a nice class browser at can be hard to follow the logic of the program (data flow and control flow). Without a nice class browser, you have no chance. If you want to see a good example, try implementing bignums in Smalltalk, with automatic failover to bignums on overflow. It's a great abstraction, but the language makes the implementation difficult to follow. Using functions on algebraic data types, the logic of your algorithm is usually all in one place, or if it is split up, its split up into functions which have contracts that are easy to understand.


P.S. What are you reading? I don't know of any responsible person who says "ADTs good; OO bad."

Norman Ramsey
I didn't say OO bad. I say inheritance is bad and almost everybody seems to be saying it. (Just a quick googling gives hundreds of results)
one-zero-zero-one
and you yourself keep praising ADTs in many Haskell / FP threads :)
one-zero-zero-one
@Cheryl: I'll back you up on that--I'm happy to say that inheritance as it exists in many languages is, from a purely OO standpoint, almost always a bad idea. Some combination of generics, interfaces, mix-ins, and object composition is almost always a much better approach.
camccann
Polymorphic variants?
Jon Harrop
+7  A: 

In my experience, what people usually consider "bad" about inheritance as implemented by most OO languages is not the idea of inheritance itself but the idea of subclasses modifying the behavior of methods defined in the superclass (method overriding), specifically in the presence of mutable state. It's really the last part that's the kicker. Most OO languages treat objects as "encapsulating state," which amounts to allowing rampant mutation of state inside of objects. So problems arise when, for example, a superclass expects a certain method to modify a private variable, but a subclass overrides the method to do something completely different. This can introduce subtle bugs which the compiler is powerless to prevent.

Note that in Haskell's implementation of subclass polymorphism, mutable state is disallowed, so you don't have such issues.

Also, see this objection to the concept of subtyping.

pelotom
+4  A: 

I am a long time OO programmer and a functional programming newbie. From my little exposure algebraic data types only look like a special case of inheritance to me where you only have one level hierarchy and the super class cannot be extended outside the module.

You are describing closed sum types, the most common form of algebraic data types, as seen in F# and Haskell. Basically, everyone agrees that they are a useful feature to have in the type system, primarily because pattern matching makes it easy to dissect them by shape as well as by content and also because they permit exhaustiveness and redundancy checking.

However, there are other forms of algebraic datatypes. An important limitation of the conventional form is that they are closed, meaning that a previously-defined closed sum type cannot be extended with new type constructors (part of a more general problem known as "the expression problem"). OCaml's polymorphic variants allow both open and closed sum types and, in particular, the inference of sum types. In contrast, Haskell and F# cannot infer sum types. Polymorphic variants solve the expression problem and they are extremely useful. In fact, some languages are built entirely on extensible algebraic data types rather than closed sum types.

In the extreme, you also have languages like Mathematica where "everything is an expression". Thus the only type in the type system forms a trivial "singleton" algebra. This is "extensible" in the sense that it is infinite and, again, it culminates in a completely different style of programming.

So my (potentially dumb) question is: If ADTs are just that, a special case of inheritance (again this assumption may be wrong; please correct me in that case), then why does inheritance gets all the criticism and ADTs get all the praise?

I believe you are referring specifically to implementation inheritance (i.e. overriding functionality from a parent class) as opposed to interface inheritance (i.e. implementing a consistent interface). This is an important distinction. Implementation inheritance is often hated whereas interface inheritance is often loved (e.g. in F# which has a limited form of ADTs).

You really want both ADTs and interface inheritance. Languages like OCaml and F# offer both.

Jon Harrop
I assume you meant the last line to read "You really want both ADTs and *interface* inheritance"?
pelotom
Oops, thanks. :-)
Jon Harrop