Previous: Documentation at scale: The principles
Programming language design must take many aspects into consideration. Fitting it's target domain, performance, style and so on.
One aspect I am personally most interested in is expressivity. Specifically: If I take a diagram from my sketchbook, is there a way to express it in the language? If I draw two boxes connected by an arrow, is there any way to capture the relationship in the language itself or do I have to resort to comments?
For example, SQL is good at expressing cardinality: For one element of this type there can be N elements of that type. Object oriented languages, on the other hand, nicely cover the "is-a" relationship: Dog is an animal. class Dog extends Animal.
Yesterday I went through my old sketchbooks and realised an interested thing: Only a little minority of diagrams (roughly 5%) were drawn to express "is-a" relationship. I am not a taxonomist, after all.
The majority of diagrams expressed communication flows, things like: "Component A speaks to component B, but not to component C."
Of course, this may be the case just for me. Perhaps majority of programmers fill their scratchbooks with class hierarchies. I doubt it, but if I am wrong, I would love to hear about it.
So, if what I said applies to most programmers, then the following is true:
Object oriented languages spend most effort addressing a minority use case.
The use case is cherished and compilers are made to discover any problems with it in compile time and produce nice error messages. At the same time other use cases are ignored, programmers have to ensure their consistency manually, by runtime checks or, even worse, by putting the constaints into comments or design documents. OO folks are wasting their time discussing minutiae of their little peculiar use case, such as, say, single vs. multiple inheritance, while at the same time nobody is paying attention to what is needed in majority of cases.
I've already written about why I switched from C++ to C (here and here) but the most fundamental reason it all boils down to is: If a complex language doesn't allow me to express 95% of what I would like to I can very well use a simpler language and save myself the headache.
Now, how are we going it fix that? And does anybody even care?
On a tangential: Have you noticed that natural languages have very little resemblance to John Wilkins' analytical language? Once again, taxonomy is one of least concerns of a user of a natural language. The fact that marmot is a rodent is simply not that interesting. You want to say what happened, express your intentions, give orders, ask questions. You want to go on with your life.
Another tangential: Despite all the misguided focus on taxonomy there's one awesome thing that OO languages have introduced: Singling out one of the function's parameters (this, self) and putting them to a syntactically special position:
arg1.function(arg2, arg3, arg4);
I've written about it here. Bear in mind that it's an article from 2006 and talks about reserach done mostly in late 1990's. I have to write something more up-to-date: If nothing else, I should mention Go's approach to the problem.
Martin Sústrik, August 15th, 2015
Previous: Documentation at scale: The principles
Nice post. But it should be called "Runtime-Polymorphic Class Hierarchies Address a Minority Use-Case." Hard to disagree with that.
Runtime polymorphism via virtual functions is useful when you need it, but frequently the wrong solution. That said, software components do have something called "points of variation". With points of variation you have two options: model them in the code, or implement a particular case. Runtime polymorphism is an inefficient way to deal with points of variation that don't change for a particular instance of a system — but it is often used for exactly that case (when all you have is a hammer…).
To be fair, C++ is not a pure OO language, it offers alternative mechanisms. I use C and C++ on different projects. Incidentally, one of the C projects heavily uses runtime polymorphism via interface tables (v-tables), and some of the C++ projects have no inheritance or virtual functions. In some cases I would find it difficult to switch away from C++, not because of class hierarchies, but because of (1) the ability to encapsulate abstract data types (e.g. with member functions, type-based overloading, and to a lesser extent, operators) and (2) the ability to work with parameterised types (templates, metaprogramming). To be sure there are languages that do these things as well or better than C++, but C is not one of them (did I read somewhere that you're doing metaprogramming in C?)
Czarnecki and Eisenecker's "Generative Programming" really cemented my views on this. Stepanov and Rose's "From Mathematics to Generic Programming" is also worth a look.
Go's approach to implicit interfaces is interesting. I haven't used it myself. It would be interesting to hear from Go users about how it compares to using explicit interfaces.
Any conversation featuring OO and ADTs would be incomplete without a reference to William Cook's "Object Oriented Programming vs. Abstract Data Types": http://www.cs.utexas.edu/users/wcook/papers/OOPvsADT/CookOOPvsADT90.pdf
Nice No true Scottsmans.
- first it's not oop but something very specific which is not related to oop at all. it's not like majority of programmers when talking about oop talk about class hierarchies
- then c++ is not "pure oop". You couldn't spell it out more explicitely
- then the Cook reference.
Don't take it wrongly, I don't disagree with you, but I am sick of classical oop, sick of class hierarchies and especially sick of people using the "no true Scottman" every time someone critizises oop (http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end)
Please stop defending it. Please stop trying to cut off the biggest slice you think is not mentioned and talk about that instead of what was criticized. It's bad. We know it's bad because we have dealt with it for decades and it's not getting better. Meanwhile, we can try out other stuff, maybe that too will be shit. Maybe there isn't a better way and we will come back to class hierarchies in the end. But first let us try out some other stuff. Right now it's basically the inertia which moves it forward, and you can't change that any other way than actually pushing against it.
This little push was cheap enough. It took me 15 mins. to write.
It would be much better to do something positive though, like looking at the scratchbooks and trying to figure out why they can't be translated to code, trying to formulate alternative constructs and so on.
There's so much work to do and so little free time :(
@totemizer:
My point was that Martin's post focused on one specific thing, and could be better named accordingly. I was not trying to neutralise criticism of OO. Perhaps I was being pedantic, but I was not defending OO.
The rest of my comment was a defense of C++, precisely because it allows us to "try out other stuff." Of course there are other languages and other techniques where even more stuff can be tried out. And no, I don't think run-time class hierarchies are ever going to come back. I do think it's folly to say "OO is bad so don't use C++" — that's throwing out the baby with the bathwater.
I don't really get the "We know it's bad because we have dealt with it for decades and it's not getting better" attitude. I hear it a lot, but it doesn't make a lot of sense to me. What exactly is bad? what are we "dealing with"? and what exactly is not getting better? Maybe you're thinking of bloated Java code, or poorly written code, but I'm just guessing.
you should favor composition over inheritance, regardless.
That's my favorite problem with OO development cycles. Those two things are not interchangeable, and not even alike. Do nails work better than an excavator?
Sustrik keeps repeating he's not a taxonomist, but OO inheritance is only taxonomy by the purest most abstract meaning this word gets on Math. OO inheritance is usually specialization. Anyway, it's still only rarely useful. But once in a while it's very useful.
Agreed. I do resort to inheritance once in a while. In my latest project (libmill) there's one instance of it.
OO doesn't need to provide a model of "Component A speaks to component B, but not to component C." since most languages handle that using variable scope: if B is in-scope for A, then A may communicate with B. If C is not in-scope for A, then A can't communicate with C. This is orthogonal to OO.
However, I would say that many classical OO languages makes this more of a headache. For example, a function which returns a dictionary of functions can use lexical scope to manage what's available to those inner-functions. A class with separate constructors and methods can't do this, so we end up with boilerplate patterns like dependency injection to emulate it.
Ack. Scopes go a long way towards the goal. Can we make them more powerful somehow?
well, we got packages. They are scopes, too. You import the stuff you need, and keep everything else outside. On a smaller scale there are closures - tho they inherit their parent scope - unnesserily?
OO design & programming seems best for certain kinds of framworks such as Microsoft Foundation Classes and packages such as javax.* When you work with such things, it becomes hard to deny the usability of OOD&P.
But when someone tries to build a solution from ground up, all those frameworks and OOD&P suddenly become useless and you have to design and code the solution using your own means; not a pre-packaged framework or a kind of programming style such as OO.
Actually, Peter Norvig in this book Paradigms of AI tried to show the futility of OO by examples; it's the Chapter 13 titles OO Programming. That chapter serves as an example showing what's OOP all about.
1. is-a is important
The is-a relationship seems very important to me. It is the basis of polymorphism and ALL reuse within programming languages. But inheritance is not the best way to provide is-a. It might even be argued that it is only tangential: the real deal is something like interface implementation, with inheritance merely a way to help filling the implementation.
Let me explain when I say "is-a" is the basis of all reuse (excepted maybe macros). The problem of reuse is to write code that can apply to object of different types (= different data representation). Now, most often your common function will make some assumption about the capabilities of the objects: its interface (e.g. this object can be serialized). Not always though: if you have a function that adds an object to a list, and you can assume uniform reference representation, then no interface is required. This is what is done in C with (void *), or why it's okay-ish that Go and Java pre-1.5 don't have generics. But for most other cases, to be able to reuse code for multiple types of objects, there needs to be some way to specify the peculiarities of each type (i.e. the interface implementation). It's a paradox of sorts: reuse requires differentiation.
Both subtyping and parametric (i.e. generics) polymorphism use the is-a relationship. Ad-hoc polymorphism doesn't… except with type classes. (But then regular ad-hoc polymorphism does not allow you to share code between different types… I actually think the name polymorphism might have been poorly chosen).
One of my biggest beef with C is that it does not support is-a. You can of course pass function pointers around but (1) it's cumbersome to add after the fact, and (2) most libraries won't ship with such capabilities.
2. Why OO is bad.
I have thought a bit why OO is bad (something that I have "felt" when using it). What I think is that the notion of decomposing code into classes is really the main ill of OO. Often, it's really not clear if an operation that involves types X and Y should belong to class X or Y. This can lead to choices that prove awkward down the line.
Also the fact that dispatch is done only on the receiver is problematic. Imagine a add(Number, Number): we might want to specialize for add(Float, Float), add(Integer, Integer), add(Float, Integer) and add(Integer, Float), but with OO we are stuck with add(Float, Number) and add(Integer, Number). The offered solution is ugly double dispatch patterns (this solution quickly becomes unmanageable past two arguments btw).
In this I seem to disagree with you that "x.(y)" is good form. Imagine "list.add(item)". You can't really say that "list" is performing the action "add" upon "item". Its more "the program adds an item into the list". But then you can have things like "transformer.apply(element)", "list.map(item)", "item.addTo(list)" and they all have different semantic meaning for the receiver and the argument. Pretending that the receiver has a different role is dangerous fiction.
As another commenter has pointed out, composition should often be favored over inheritance, but I'd like to add that you can't just change your "is-a" relationships by "has-a" and be done with it. You need to structure your code differently (divide it into classes differently) for this to work. With all the pitfalls such a division entails in OO.
If I were to point out to a better solution, I'd say Haskell typeclasses (I mostly program in Java, ironically). I'm currently doing some work related to improving typeclasses for a language I'm working on.
3. Questions
Could you explain what you meant about components being able to speak to some others but not to some others? You said above that scope helped but needed to be more powerful. How so?
Would something like the ability to export functions exclusively to some parts of the program be in line with what you are seeking? (e.g. "functions X, Y and Z can only be used by module A and B") Such controls currently exist but are coarse-grained, for instance Java has package-local and protected (package-local + subclasses). Actually C++ friend classes seem pretty close to what I suggest (although it applies to all methods within a class).
You should search for clojure multimethod if you are interested.
Well, let us not forget of Data flow languages. Their focus is to express who talks to who.
https://en.wikipedia.org/wiki/Dataflow_programming
THe rise of muticore computing has put it back into the mainstream.
It's worth noting that class diagrams are just one view of a system. UML has a bunch of different diagrams:
https://en.wikipedia.org/wiki/Unified_Modeling_Language
Collaboration Diagrams capture the "who talks to who thing":
http://agilemodeling.com/style/collaborationDiagram.htm
@Martin: it would be interesting to hear about the properties you're trying to capture that aren't expressible in a collaboration diagram.
Saying inheritance is equal to OOP is like saying currying is equal to functional programming. Most real-world applications rarely use inheritance, and it's only a tiny aspect. After all, it's called "object-oriented", not "class hierarhcy oriented". In that regard, I think the rant against OO languages is unfair.
Not to mention that there is barely a purely object-oriented language anymore. Java, C#, Scala - all of them have a functional aspect as well.
So I neither get which languages are you referring to, nor why you limit the usecases they to just a fraction of what they really cover?
Post preview:
Close preview