How Class-based Programming Sucks (2011)
loup-vaillant.fr
loup-vaillant.fr
The article is chasing down the wrong tree when he tries to build an Option type in C++, though. The normal OO way to handle the same class of functionality as ADT sum types is to use separate subclasses. What he's not acknowledging is that OO and functional+ADTs both only handle half the expression problem[1], in different ways. There's no absolute superiority for either model. It depends on the domain.
Less mutability is good almost everywhere though. The lack of persistent data types is a blight on almost all non-functional container libraries.
That and games (and I'd wager simulations in general). The only two domains I'm familiar with which I cannot imagine without OO.
From what I've seen, even UIs and games written in languages without support for OO still use something very close to OO. GTK for instance takes it quite far with GObject.
[1] http://research.microsoft.com/apps/pubs/default.aspx?id=1793...
Some of that's admittedly just down to the window dressing. Object-oriented abstraction mechanisms such as interface tend to have a measure of self-documentation built in, to the extent that all the members are named, right down to method parameters. There's no reason you couldn't do something similar with FP, it's just that as far as I can tell nobody's ever made a point of doing so.
Actually it is a very good way to create ASTs if you need them to be extendable.
Which is exactly the use case where algebraic data types fail short.
Discriminated unions on the other hand are - the name says it - intended for cases where you want to discriminate between multiple possible values. They're by design not intended to hide differences behind an interface; they don't address the same problem at all.
All in all, I think that neither classes nor discriminated unions solve the interface problem (nor were they ever really designed to do so) - that's a different problem.
I just meant OO in general, not classes specifically.
It's a question of terminology whether you call that object-oriented. It's certainly part of OO, in any case.
See my comment about the expression problem in another thread: https://news.ycombinator.com/item?id=6783075, as well as this response: https://news.ycombinator.com/item?id=6784057.
A compiler is a program that consists of lots of different algorithms (operations) that operates on a pretty well-defined set of data (an AST or an IR). Changes to the data representation are rare compared to changes to the operations on that data.
What OO does in a compiler is totally obscure the control flow of each algorithm by spreading the logic out over dozens of classes. You can look at an AST node and see at a glance how it participates in constant folding or code generation, but if you're trying to optimize the constant folding algorithm or the code generator, you've got to follow control flow across dozens of different classes.
Using visitor pattern mitigates this somewhat, because you can have ConstFolder::visitBinaryOperation, ConstFolder::visitFunctionCall, etc, all in one file, but note that visitor pattern is just a really verbose, roundabout way of writing a switch statement! If you add a new type of AST node, you have to add a visitNewAstNode call to the visitor interface, and then go update every class that implements the visitor interface. This is no easier, and a lot more verbose, than simply adding a new variant to an AST ADT then fixing up all the places where the compiler complains that your pattern match is no longer exhaustive.
For a good example, look at how LLVM does instruction simplification: http://llvm.org/docs/doxygen/html/InstructionSimplify_8cpp_s.... A good old switch statement, no "simplify" method in the "Instruction" class nor visitor pattern.
But if you want your control flow to be in place, then a 10,000 line case match ala scalac should be right up your alley. Frankly, I'd rather divide and conquer, which is what OOP is good at.
The expression problem is much safer solved using virtual classes (using traits and type members) in scala than with case matching. There are so many OO solutions to this problem it's not funny; I think I had a small argument with Wadler over this back in 2001 or 2.
As for persistent collection types, observable collections with undoable operations are just as good, if not better because they can still support mutability while persistent collections cannot.
I think persistent collections, along with nice syntax for updating immutable objects (i.e. cloning with a subset of changed attributes) are more practical than having undo available. Immutable data types remove the burden of worrying about who's going to modify the data type from under you, so it lets you share subgraphs more freely. You don't need to worry as much about coupling, because the things you give your state to can't modify it. A possible alternative is mutable collections with snapshots or freezing, but I think persistent collections are a better approach.
I'd love to hear more detail about your experience on this point.
I've built a live programming environment where reactive mutable collections are much more appropriate than immutable persistent collections, you can check it out here:
http://research.microsoft.com/en-us/people/smcdirm/liveprogr...
The problem with immutable collections in general is that they completely cannot track change deltas, which is necessary when building an incremental systems. Undo is also essential for these kinds of systems. I think persistent collections are a dead end, but its an argument I'll have to work on over the next few years.
[X] is nice but [Y] will get you most of the way and [dealing with the difference] is simple.
reads a lot like the Blub conceit. I'm not accusing you of that, but I do disagree that OO can give you what pattern matching typically does.
As long as our hypothetical Blub programmer is looking down the power
continuum, he knows he's looking down. Languages less powerful than Blub
are obviously less powerful, because they're missing some feature he's used
to. But when our hypothetical Blub programmer looks in the other direction,
up the power continuum, he doesn't realize he's looking up. What he sees are
merely weird languages. He probably considers them about equivalent in power
to Blub, but with all this other hairy stuff thrown in as well. Blub is good
enough for him, because he thinks in Blub.
From PG's Beating the Averages (http://www.paulgraham.com/avg.html)I also have no problem with the expression problem. C# has partial classes anyways, which work even if type checking is non-modular. Of course, I would like it if C# support some form of pattern matching, but not enough to switch over to F# (whose pattern matching isn't as rich as Scala's, anyways).
No, I would have quoted it the first time if it was. But if you want to repeat that again you will certainly sound conceited.
It's my fault for trying to engage an HN-er in a form of discourse other than debate.
Perhaps. But then again the "Blub conceit" is nothing that exists in real life as a proven fact of computer science.
It's just an argument expressed in an essay. Not some kind of formal logical error.
Might as well say "your argument reads a lot as something that an unbeliever in the flying spaghetti monster would say".
Why do you think this? I prefer having an open trait that's only inherited by ADTs.
I can get non-exhaustive match warnings and I still 'solve' the expression problem by matching on a superset of my partial functions (that do the matching).
When I did this in Scala (using traits and type parameters in a virtual class pattern), I got a lot of pushback from the FP people, who thought this problem couldn't, or more correctly SHOULDN'T, be solved using object-oriented constructs; it was one of the main things they liked to boast about in how FP was better than OOP, and I literally took that away from them. A 10,000 line pattern match was somehow preferable to a file with 20 traits that described how the new operation was performed per variant. Martin even made my pattern illegal via a new type check in the Scala compiler after I left EPFL :)
I've programmed with 3 approaches to UI: every things a subclass, instance of class, and prototype-based objects. I enjoyed programming UI on the Newton because of the prototype-based programming. It felt natural. NeXT / Apple's instance of a class works very well too.
Every time I see "every things a subclass", I wince and know its going to be a pain in the butt.
> "Once upon a time, there was a company Taligent. Taligent was created by IBM and Apple to develop a set of tools and libraries like Cocoa. About the time Taligent reached about the peak of it's mindshare, I met one of its engineers at a trade show. I asked him to create a simple application for me: A window would appear with a button, and when the button was clicked, the words 'Hello World!' would appear in a text field. The engineer created a project and started subclassing madly: subclassing the window and the button and the event handler. The he started generating code: dozens of lines to get the button and text field on to the window. After 45 minutes, I had to leave. The app still did not work. That day, I knew that the company was doomed. A couple of years later, Taligent quietly closed its doors forever. "
At the end of the day, pick what language and coding paradigms best suit your problem domain and you as a coder. Pick right, and there's no mistake there.
In the real world, everything is a process. Nothing ever stays the same or stands still. No object is the same object from one millisecond to the next.
Immutable state isn't just a formal exercise--it's a more authentic model of the world.
To me that's as bad as saying "everything is an object".
I'd rather try and understand where each approach is best rather than picking a single approach and dogmatically applying it in every scenario.
Now it turns out to be difficult to maintain synchronized clocks, and Lamport timestamps and vector clocks are alternatives. The end result looks similar to a relativistic situation, but (and I'm not a physicist), it seems wrong to claim this situation is because of Special Relativity.
Thoughts?
In practice, replace Special Relativity with Network Problems.
http://ocw.mit.edu/courses/electrical-engineering-and-comput...
Been reading up on Heraclitus lately?
While this is an interesting statement for a philosophical dialogue, it doesn't really make much sense in the context of programming, where many things are constant, many things are not processes, and there are plenty of use cases for both mutable and immutable state (which is exactly why I always get really defensive reading articles like this one, that pretend all problem domains would be best modelled by a single programming language/paradigm/model of computation).
Surely that is in part due to the developer. Poor developers will choose the wrong abstractions, and have a poor OOP model as a result. Better developers will choose the correct abstractions, and have better models. Obviously the domain you are modeling will make it easier or harder to model things, and in some cases OPP may not be a good choice at all.
In my day to day work I use Django. I start with a data model - based on an ER diagram. I code that in the Django model classes. Is that OOP modeling or ER modeling? I don't know. I don't really care too much. It works fairly well for most things I am doing.
This is important, and something almost everybody ignores.
Get an OO codebase, and try to implement a time-dependent feature, like snapshots, or even a simple undo. The abstractions fall apart.
I bet the authorities around the world don't like your time-inclusive-dimenstion view much.
No matter what you do, identity does not change. You commiting a crime yesterday was an event that included you as an object with identity and today you have the same identtiy so you are clearly responsible for what you did yesterday.
GUARDS! :P
If space-time is discontinuous, then we can think any change of state, like motion, as a set of discrete changes. If we think the motion of a particle from one energy bin to another is immediate (meaning the particle cannot be found on the border between the bins, or one moment the particle is in Bin A, next it is in Bin B) then we can think the particle was destroyed in Bin A and another one was created at Bin B. And this is what we call motion from A -> B.
I don't know, I think it's a point where education fails. Your average enterprise developer should be forced to read books like "Clean Code" by Bob Martin before being allowed to touch a keyboard.
I'd argue that the problem with software quality is not so much in the (ab)use of mutable state, but the fact that (especially in enterprises) software often reflects the organizational structure of the people who worked on it.
Exhibit A: the software solution where every tiny part of the system is developed by some (semi) random group of people that often changes, doesn't directly communicate with any of the other groups, doesn't bother to much with the 'architecture' of the complete system (a big ball of mud), and has no interest improving either the overall architecture of the system, or any of the components outside their scope. Add pervasive mutable state into this mix, and you have a recipe for disaster.
That's not to say the mantra 'eliminate (almost) all forms of mutable state' will improve this problem much.
See what I did there.
Many times I've heard this argument from someone who only knows the one or two tools they learned in college or in their first job, and they try to dress up their ignorance as wisdom for "not wasting time on the wrong tools".
I recommend http://norvig.com/21-days.html as a great place to start (I have only learned, at best, 3/6 of his suggested language categories, and I am on the 4th).
To me, it makes much more sense to try to create programming languages that are able to express human thought. People think in terms of things that are, to a first approximation immutable, memories are a good example.
You can have OOP without classes - prototypal inheritance for example. In JS, you just clone an exemplar with Object.create() for example.
That said, the article author does the same thing.
That's only true on a superficial level. Sure, in CS 101 it's easy to explain how to make a Point object and add a Move() method to it. But dive into any real-world code base and you're more likely to meet RectangleCollectionColorPickerFactory instead. Let's face it, writing a large program is an exercise in abstract symbol manipulation. Trying to make it correspond to "real" objects is about as hopeless as basing literature on coloring books.
Trying to make code correspond to real world objects is annoying because after you've modeled your Car with Wheels that support Tires, and an Engine with EngineParts attached ... you then have to write a adaptation layer to make all that fit into your storage engine, your user interface, your query engine, etc ad nauseam. Strings are not "real world" objects, but we use them in our code nonetheless. They're a great UI item that lets us interface with our users. "I need input from the user. Here, type some text in this field and I shall call it String." Now you can manipulate the String with operations (methods on the object) that make sense for Strings.
Point: OO is not about "modeling the real world," it's about code organization. Some people organize with OO. Some with other means.
There is of course an alternative: use a function rather than a factory; but that's kind of the point of this discussion :-).
(Before we start a flame war: This isn't a criticsm of ruby or C# specifically, it's just something I've seen a lot in those code bases. Both languages allow writing factories or using lambdas.)
And I entirely agree with you that a PointFactory is unwanted (barring special circumstances) - though I entire disagree you'd want points to be mutable. Certain control points? Certainly - make a MutablePoint which is (conceptually) simply a reference to a point value. All points? That's just asking for pain; I really have better things than to track down with nested submodule thought it was mutating a copy but due to some optimization interaction turned out to be mutating a copy someone was actually looking on. Not to mention that reference semantics don't work very nicely with hashtables and lots of other datastructures which become a lot more complicated when the values can change right under them.
>> That's only true on a superficial level.
Exactly. I had forgot that I learned OOP by modelling a CD collection. When thinking about it now, "mapping" a program into real-world models and terms seems totally backwards.
What's the point of thinking in real-world models unless you program actually manipulates real-world objects? Of course there are properties that are useful to carry over, but why limit your program to real-world models?
Something like "RectangleCollectionColorPickerFactory" is obviously a confusing and ridiculous concept, and exemplifies a failed application of OOP, not a failure of OOP itself. One could invent equally absurd demonstrations of functional programming, or indeed of any other paradigm.
I only really "got" design patterns when I realized that the strategy pattern was just treating an algorithm/implementation as an object and stopped thinking that OO was exclusively about mapping things to real world objects.
> See, mutable state makes shorter English sentences, and the agent concept helps make analogies with our fellow humans. In the end, this first impression trumps the fact that avoiding mutable state where possible ultimately yield simpler programs.
- But shorter English sentences are awesome.
- Good analogies are awesome.
- Effectively communicating with our fellow humans is awesome.
Indeed, all these things contribute to simplicity.
I find that often critiques of OOP are actually critiques of poorly implemented OOP. When it's done well, it can retain many benefits of immutability while still mapping more naturally to domain concepts. This talk by Gary Bernhardt is a great discussion of using functional concepts in OOP, and doing OOP well:
https://www.destroyallsoftware.com/talks/boundaries
The talk "Functional Core, Imperative Shell" is also a must see.
You missed the author's point here. He of course prefers shorter sentences as well. He is complaining that mainstream language syntax is biased in favor of mutability since you need to use an extra keyword to make a const expression. In F# for example it is just the opposite, you have to use the keyword "mutable" in your declaration.
An addendum: there is often no single obvious best way to do something. Though many approaches are obvious as being less wrong than others.
Also note that theories that have tried to categories real world objects into classes have a tendency to fail (although they also have a tendency to become enormously popular [see Platon and Aristoteles fx.]).
So the claim that classes allows you to model a problem in real-world terms, is most likely false.
See for example http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.56.... for a review.
It absolutely reduces cognitive overload. Having "containers" of functionality that allow me to easily know at a glance what I'm working with is invaluable. I'm not knocking functional programming, since I haven't built a few projects in a purely functional language yet, but trying to do some things with the functional paradigm (or at least, my understanding of it) left me recognizing class-based programming as a powerful tool when used correctly.
He then proceeds to launch his piece of chalk across the room! (good stuff). And proposes that instead of the chalk having changing state (position, velocity, etc), instead, its better to thing of the chalk as a collection of immutable values; each value existing at a moment in time. And this of course aligns more to functional programming; and you see the same notion (immutable values over time) with descriptions of Datomic.
[1] http://ocw.mit.edu/courses/electrical-engineering-and-comput...
1. Dynamically Typed Languages (LISP, Erlang, Smalltalk, Python, Ruby, and Javascript) are all easier than Haskell, JAVA, C++ or any other typed language for working with ADTs. Whether the language is OO or not isn't really relevant. On the other hand, you loose type-safety/efficient representations. But life is filled with choices, different tools are good for different things at different times.
2. You can have closures in a class based language. Here's some rather exotic Python code that uses both for measuring the performance of iterators: https://github.com/wearpants/measure_it/blob/master/measure_...
Smalltalk and Scala also have closures. It's not an either/or for classes vs. closures in programming language land.
3. There's more to concurrency than shared memory, so immutability is again a weapon that is good for some battles. If you have an app which you've factored into workers, unless they are on the same machine they'll need to communicate. Having your data be serializable is more important than having things be immutable in this context. What a worker does with data while it is handling it can involve lots of mutation.
So everyone should take a big breath, and remember that there's no one way to think about software and there's no universal rules, other than probably P != NP and stuff like that.
You can map instance methods to normal methods in Smalltalk. And static methods to class methods in Smalltalk.
Of course, you don't have the dynamism of sending messages, instead of invoking method calls.
However given that Smalltalk is the originator of OO and everything is an object, OO critics would feel in prison most likely. :)
Does anybody understand what the author means by this? STL's algorithm library does not use class hierarchies for anything. And they often take functions (be they free functions, lambdas, or functors) as arguments.
In C++ a class is just a facility of the language with many different features. If you want immutable, value-semantic objects, then do it. If you want a closure... well objects can be closures too. C++ calls them functors, they can be immutable. In fact, C++11 lambdas are immutable by default.
His criticism of the STL is probably that they expose their mutation, rather than hiding it in the architecture like pure functional languages. In C++ you just have to hide the infrastructure yourself.
That being said, nearly all C++ applications can benefit from immutable classes, algorithms that take closures, and so on. But C++, like C before it, is really just an abstraction of the hardware and OS below it. That hardware mutates. That pool of memory changes size and locality. The closer your problem domain is to the metal (especially drivers and highly-optimized code), the more you appreciate mutable state, at least given the current state of computing.
Now with C++11 lambdas, std::algorithm is much nicer to use.
But if it gets more use in the mainstream that way, then so be it.
Many large applications are implemented as a thin user interface (GUI, web, services/APIs) which glues together a bunch of libraries (which is where all of the real functionality is implemented). This doesn't only apply to F#, but C#, Java, Python, etc.
The reason F# gets used for implementing libraries is because that makes it easier to introduce into organizations with existing code in C# or VB.NET; a new component or plugin can be written in F# and easily utilized by the existing code, so the other developers in the organization don't need to anything differently than if they were consuming another C# or VB.NET component.
But I fully understand it, on my enterprise world JVM == Java and .NET == C#, except when doing small prototypes. It is very hard to use alternative languages, as managers always look for coding drones.
Technically, ML is where it all started. Standard ML and OCaml are to ML as Scheme and Common Lisp to Lisp.
PS. Just submitted to HN - https://news.ycombinator.com/item?id=6900426
I suppose most folks here disagree?
The most important thing in my opinion is tools and platform support. Show me a fast, good looking IDE with good autocomplete/intellisense/integrated debugger and UI tools for ML. See?
Is there an abundance of libraries and api wrappers available (that doesn't require you to do straight C-interop)? See?
Also: the design of a language should be done with the tools in mind. While there is not much difference between norm(vec) and vec.norm() the latter is the form that supports autocomplete. So even for functional languages, being able to use member properties and member functions is an absolute reqirement to support the tooling that we expect.
You could say that F# dominates C#. It has almost the same tool and library support, while having the features you expect from an ML type language.
The difference between norm vec and vec.norm() is that the function can be used in a higher order function while the method would require wrapping it in a lambda to achieve the same thing and would probably still break your tool's ability to autocomplete if you used it that way.
You're basically insisting that an FP language needs to include an embedded OO language to be usable.
The obvious conclusion is that any statement that contains the phrase "the obvious conclusion is" is 100% BS, including this one.
There are other problems with Object-Orientation. For example, it tends to scatter allocation which has performance impact. ( see this nice presentation: http://harmful.cat-v.org/software/OO_programming/_pdf/Pitfal... )
If performance is not an issue, I'd guess the price you pay for OO is that you eschew parallelism and concurrency.
Also, as soon as state escapes a function, it becomes shared, so should be immutable (except when explicitly made mutable).
What I found out pretty quickly is that, especially as I was running my code on a mid 80s mini computer, I had to spend as much time on allocation and garbage collection strategies as anything else. Indeed, my only really "difficult" bug was caused by my premature re-use of application nodes - I thought I was being clever re-using them immediately but it caused problems months later when I started writing recursive expressions using the non-native Y-combinator. I had no idea what was causing the problem and was really quite worried, I was stumped for days and then I had a flash of insight from nowhere while sitting on a bus that fixed the problem.
We're on tenterhooks here...
What I always remember is that the idea as to what was causing the problem came as a complete bolt from the blue when I was thinking about something else - perhaps the first time that something like that happened to me, but certainly not the last!
On the other hand, there are languages that make it easier for you to screw it up and the ones that try to prevent that.
But there's no bullet proof language.