October 21st, 2024

OOP is not that bad, actually

The article compares Object-Oriented Programming (OOP) and Haskell's functional programming, highlighting OOP's advantages in collaboration, library extension, and backward compatibility, while noting Haskell's challenges in these areas.

Read original articleLink Icon
OOP is not that bad, actually

The article discusses the merits of Object-Oriented Programming (OOP), particularly in statically-typed languages, and contrasts it with functional programming approaches, specifically Haskell. The author acknowledges that while OOP may not be their preferred paradigm, it offers significant advantages for collaborative programming over extended periods. Key features of OOP, such as classes, inheritance, subtyping, and virtual calls, facilitate the development of composable libraries and allow for backward-compatible extensions. The author illustrates this with a logging and database example, demonstrating how OOP enables the addition of new functionalities without altering existing code. In contrast, the author explores how similar functionality could be achieved in Haskell, highlighting the challenges of maintaining backward compatibility and the complexity introduced by type parameters and effect monads. The article concludes that while Haskell offers powerful abstractions, OOP's straightforwardness in evolving libraries without breaking existing code is a notable advantage.

- OOP provides a structured approach to programming that supports collaboration and long-term maintenance.

- Key OOP features allow for backward-compatible library extensions without requiring changes to existing code.

- Haskell's functional programming model presents challenges in achieving similar flexibility and backward compatibility.

- The article emphasizes the trade-offs between OOP and functional programming paradigms in practical software development.

Link Icon 37 comments
By @whizzter - about 1 month
The author brings up the basically the canonical example of where OOP style design shines, and where functional programming will falter.

The simple truth however is that overly going into either functional or OOP camp will hurt because strict adherence becomes subscribing to a silver-bullet.

The middle road is simply a better engineering option, use a practical language that supports both paradigms.

Keep data transforms and algorithmic calculations in functional style because those tend to become hot messes if you rely overly on mutation (even if there is performance gains, correctness is far far easier to get right and write tests for with a functional approach), then there are other concerns where an OOP derived system with inheritance abstractions will make things easier.

By @kerand - about 1 month
The GoF book did a lot of damage. I've finally read it and was amazed that the entire book is in fact about writing GUIs, which is just one tiny part of programming.

Several patterns are trivial, others are very similar and are just a linked list of objects that are searched for performing some action.

The composition over inheritance meme in the book does not make things easier (there is no "composition" going on anyway, it is just delegation).

Objects themselves for resource cleanup like RAII are fine of course.

By @amluto - about 1 month
This article describes the OOP approach of changing, say, a Logger to an abstract class with a default implementation, without needing to modify code that uses it, and also describes this as something that could be done in C++. But this doesn’t work at all in C++ if there is ever a Logger on the stack!

    Logger l;
That’s a Logger, and it will not magically become a subclass.

And changing this to go through pointers everywhere and to use virtual functions, in C++, is not very performant. A good JIT compiler may be able to effectively devirtualize it, but C++ compilers are unlikely to be able to do this effectively.

By @Rayhem - about 1 month
I've come to understand that software has two axes: the problem domain and the software domain.

The problem domain is things like your physical model. Your 3d mesh. Financial transactions. All things that need to be represented in software.

The software domain is things like components and queries. Things you can build in-software to realize your problem domain on a computer.

Defining a component and the behaviors of a component has nothing at all to do with the problem domain; it's purely an artifact of software to make your system more extensible/reliable/faster/whatever. But this is the part that's OOP excels at because you get to write the rules of your component taxonomy in its entirety. You have a ThingDoer class that can accept any number of Components to give it the behaviors you need, and then you start writing out components to model your problem domain. The objects and their behaviors remain at the level of "how do I put these pieces together". OOP sucks for the problem domain, though, because you're always stuck trying to munge some limited inheritance tree (animal <- dog <- wolf) onto the Real World and it's always going to miss something. Far better to build atomic building blocks that are expressive enough to compose into whatever you need.

By @userbinator - about 1 month
In moderation, yes. The problem with OOP, like all other paradigms that came before, is applying it dogmatically and excessively, which always happens when there's a lot of hype, novelty, and $$$ to be made.
By @DeathArrow - about 1 month
Eric Lippert, one of the designers of C# language, wrote a series of articles about object oriented design problems.

Here's the first article in the series: https://ericlippert.com/2015/04/27/wizards-and-warriors-part...

By @carapace - about 1 month
I sure am glad that, when I was twelve and learning Pascal at middle school, my teacher took pains to point out that OOP is just a way of arranging code, it doesn't change the code semantically, it's just topological. You avoid a lot of noise and nonsense if you just keep that simple idea in mind: OOP is a style of arrangement, not semantics.

It's especially odd to compare and contrast OOP style with Functional Programming paradigm because these things are orthogonal.

By @raincole - about 1 month
OOP is not fundamentally bad or good. But I had some first-hand experience about how it's taught "wrong".

The following might sound ridiculous, but I swear I'm not making them up:

- In my highschool, students on their "Computer & Information 101" class were asked to answer what polymorphism is. Most of the said students had zero programming experienece at the time.

- In my sophomore year (CS major), students were asked to finish a mini game "with design patterns" and explain what design patterns they used. For most of the said students, that was the first time they wrote a program with more than 300 LoC. Before that, all the assignments they had seen are "leetcode-like", like implementing Sieve of Eratosthenes in C.

By @o_nate - about 1 month
I think its important to be aware of functional style and the weaknesses of OOP, so you can write OOP code that avoids the worst pitfalls. Classes still remain a nice way to organize code, but you should try to make them immutable if you can.
By @pyrale - about 1 month
The post's conclusion looks like the author has an axe to grind:

> I think it would be beneficial for the functional programming community to stop dismissing OOP’s successes in the industry as an accident of history and try to understand what OOP does well.

But the author has spent enough time in the haskell ecosystem, and probably has some cause for this statement. I would personally have liked to hear more about that cause, and the perceived issues in the community, rather than code examples.

By @bhouston - about 1 month
I changed my mine on this. I did OOP since the mid-1990s when I learned it in high school up until about 10 years ago. I find OOP works best when you have a single coder who can store the model of the system in this mind and work out how to design the base and abstract classes well. And they also have freedom to refactor THE WHOLE CODEBASE when they get it wrong (probably multiple times.) Then you can make these webs of elegant ontologies work.

But in real life, when there is a team, you run into the fragile base class [1] constantly and changing that base class causes horrible issues across your code base.

I have found that OOP with inheritance is actually a form of tight coupling and that it is best to not use class hierarchies.

I agree with encapsulation and modularity and well defined interfaces (typed function signatures are amazing.) I just completely disagree with inheritance in all forms.

There are no benefits to it (besides feeling smart because you've made an elegant but ultimately brittle ontology of objects and methods), just a ton of downsides.

[1] https://en.wikipedia.org/wiki/Fragile_base_class#:~:text=The....

By @mekoka - about 1 month
Every code base that I've read that made faithful use of OOP artifacts such as inheritance (in all its forms) has been made more difficult to understand because of it, rarely despite it.

OOP certainly has good features (e.g. encapsulation of state), but I think it tends to shine best when programmers are really aware of the trade-offs. Most aren't. The same person that agrees that mixins are a bad idea in React, will then turn around and happily organize their logic as class-based views in Django.

And due to sunk cost, it's nearly impossible to convince someone who's invested time in this paradigm that the acrobatics are often probably unnecessary.

In my opinion, newer languages expose programmers to better mental models than "the class hierarchy" to solve code organizational problems. Work with Go or Elixir for a while and see your Java and Python improve.

By @kwar13 - about 1 month
Going from C++ to Rust, I now really love how Rust implements "OOP". I highly suggest reading this chapter even if you don't care about Rust.

https://doc.rust-lang.org/book/ch17-00-oop.html

By @ildon - about 1 month
I noticed there's an entire paragraph explaining what OOP is, but it might be helpful to clarify that OOP stands for Object-Oriented Programming. Even though it's a well-known acronym, adding that explanation could benefit readers who are new to the concept.
By @dboreham - about 1 month
Although short, this article is quite interesting because it presents code examples from "both sides" and the author seems to have a good understanding of both.
By @pull_my_finger - about 1 month
Old school OO languages where they had to use classes and objects to patch missing language features probably did suck. OOP as an abstraction API on a modern, type annotated language is really nice and intuitive. Anyone in doubt, but open-minded should be encouraged to checkout a language like Pony[1]. Although it _would_ be nice to have first-class functions instead of the lambda objects they have it's otherwise really nice. No inheritance, real type interfaces and traits instead of "abstract classes". Combine a modern language like Pony with (mostly) sane modern OO advice like in Elegant Objects[2] and you're finally cooking with grease.

[1]: https://tutorial.ponylang.io/types/classes

[2]: https://www.elegantobjects.org/

By @DeathArrow - about 1 month
GoF patterns, Martin Fowler books and Uncle Bob books had the same impact to programming as the invention of null.
By @kreyenborgi - about 1 month
The r/haskell comments show some simple alternative Haskell implementations of OP's OOP logger, e.g. https://old.reddit.com/r/haskell/comments/1fzy3fa/oop_is_not... and https://old.reddit.com/r/haskell/comments/1fzy3fa/oop_is_not..., I'm wondering if the post author just did not realize you could use a partially applied function; I still don't see what value OOP gives here.
By @d_burfoot - about 1 month
One very important issue in OOP is packaging together variable names. You can see the issue by looking at this atrocious function signature from the Python Pandas library:

> pandas.read_csv(filepath_or_buffer, *, sep=<no_default>, delimiter=None, header='infer', names=<no_default>, index_col=None, usecols=None, dtype=None, engine=None, converters=None, true_values=None, false_values=None, skipinitialspace=False, skiprows=None, skipfooter=0, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=<no_default>, skip_blank_lines=True, parse_dates=None, infer_datetime_format=<no_default>, keep_date_col=<no_default>, date_parser=<no_default>, date_format=None, dayfirst=False, cache_dates=True, iterator=False, chunksize=None, compression='infer', thousands=None, decimal='.', lineterminator=None, quotechar='"', quoting=0, doublequote=True, escapechar=None, comment=None, encoding=None, encoding_errors='strict', dialect=None, on_bad_lines='error', delim_whitespace=<no_default>, low_memory=True, memory_map=False, float_precision=None, storage_options=None, dtype_backend=<no_default>)

An OOP approach would define a Reader object that has many methods supporting various configuration options (setSkipRows(..), setNaFilter(...), etc), perhaps using a fluent style. Finally you call a read() method that returns the DataFrame.

By @jy14898 - about 1 month
> However, unlike our OOP example, existing code that uses the Logger type and log function cannot work with this new type. There needs to be some refactoring, and how the user code will need to be refactored depends on how we want to expose this new type to the users.

This is odd to me. There are solutions to make data types extensible in haskell, but for example in purescript you'd just use a record. But more importantly, why would you want existing functions to use the new type? Why not just pass the _logger from FileLogger in? The existing functions can't use the _flush ability anyway (in both OOP and FP cases)

By @enugu - about 1 month
This post doesn't seem like a criticism of FP so much as the module system in Haskell, ML has a more powerful module system.

On r/haskell, user mutantmell gave a implementation of the code (assuming such a module system) closely following the Dart code given in the original post.

https://gist.github.com/mutantmell/c3e53c27b7645a9abad7ef132...

https://www.reddit.com/r/haskell/comments/1fzy3fa/oop_is_not...

https://www.reddit.com/r/haskell/comments/1fzy3fa/oop_is_not...

>(Java-style) OOP offers a solution for this: you can code against the abstract type for code that doesn't care about the particular instance, and you can use instance-specific methods for code that does. Importantly, you can mix and match this single instance of the datatype in both cases, which I believe to be a superior coding experience than having two separate datatypes used in separate parts of your code.

>"Proper" module systems (ocaml, backpack, etc) offer a better solution that either of these: when you write a module that depends on a signature, you can only use things provided by that signature. When you import that module (and therefore provide a concrete instance of the signature), the types become fully specified and you can freely mix (Logger -> Logger) and (SpecificLogger -> SpecificLogger) functions. This has the advantage of working very well with immutable, strongly-typed functional code, unlike the OOP solutions.

>This is in essence the same argument for row-polymorphism, just for modules rather than records. It can be better to code against abstract structure in part of your code, and a particular concrete instance that adheres to that structure in other parts.

By @nashashmi - about 1 month
> Subtyping, where if a type B implements the public interface of type A, values of type B can be passed as A.

I am confused by this statement or it is going against what I understand.

If you have created a Type B variable, and you also have a new interface called A, and B implements A, then why would Type B variable's values be passed to A's values. 'A' is only an interface.

By @ThinkBeat - about 1 month
It is all just fashion and fads. Use whatever you like best (if the situation permits) otherwise use whatever you are told to use.

"It is not difficult to keep a functional aspect in OO."

"It is not that difficult to do OO in Pascal or similar" (quote from Wirth)

Several functional programming languages includes an OO or OO like feature.

Nothing will fit every situation better than anything else.

By @2OEH8eoCRo0 - about 1 month
Working in the kernel or with drivers I see a lot of "object-oriented" finagling in C where there will be structs of function pointers alongside pointers to data to operate on. Isn't this the best argument for something like OOP? You're basically creating an object at that point. Am I missing something?
By @epolanski - about 1 month
The latest state of successful FP projects can be found not in Haskell (which, for all its hype has still to post one killer software when even PHP has many) but in effect systems ala effect-ts or ZIO.

OP's example is trivially solved by services in both, because both treat dependencies as first class constructs and types.

By @Hashex129542 - about 1 month
I was actually fan of OOP languages particularly C++ & Java but no improvements so far on the main stream programming languages. Still there are lot of improvements need to do. Still C occupies the first place.

PS: I really hate python style paradigm & declarative programmings. Rust is top of my ignore list.

By @jauntywundrkind - about 1 month
Dark Side has such powerful allure, such temptation; they are strong emotions that leave such indelible marks.

And I feel like code culture is one place in need of some checks, on it's checking. There's so many wide-ranging beers out there, prejudices. Some have fought those battles & have real experience, speak from the heart. But I feel like over time the tribalisms that form, of passed down old biases, are usually more successful & better magnets when they are anti- a thing than pro a thing.

JavaScript, PHP, Ruby, rust. Systemd, PipeWire, Wayland. Kubernetes. OOP, CORBA, SOAP. These are examples topics are all magnets for very strong disdain, that in various circles are accepted as bad.

It's usually pretty easy to identify the darksiders. Theres almost never any principle of charity; they rarely see in greys, rarely even substantiate or enumerate their complaints at all. I've been struggling to find words, good words, for the disdain which doesn't justify itself, which accepts it's own premise, but the callous disregard & trampling over a topic is something I would like very much to be a faux pas. Say what you mean, clearly, with arguments. Manage your emotional reactions. Don't try to stir up antagonism. If you can, cultivate within yourself a sense of possibility & appreciation, even if only for what might be. Principles of Charity. https://en.wikipedia.org/wiki/Principle_of_charity

I'm forgetting what else to link, but around the aughts this anti- anti-social behavior has a bit of a boom. Two examples, https://marco.org/2008/05/21/jeff-atwood-who-knows-nothing-a... https://steveklabnik.com/writing/matz-is-nice-so-we-are-nice...

(And those on the pro side need to also have charity too.)

The idea of the Speaker For The Dead, someone who tries to paint clearly both upsides and downsides of a thing, is one I respect a lot & want to see. A thing I wish we saw more of.

(I feel like I have a decent ability to see up and down sides to a lot of the techs I listed. One I'd like better illumination on, a speaker for the dead on: CORBA.)

By @DeathArrow - about 1 month
For me, the the antidote to OOP everything isn't pure functional programming, but data oriented programming, where your data and the code that performs operations on it are separated.
By @odyssey7 - about 1 month
Two words which might change your life: equational reasoning.
By @sirwhinesalot - about 1 month
Meanwhile us procedural folk think you're all silly.
By @stonethrowaway - about 1 month
By @jerf - about 1 month
Having chewed on this for a while now, my personal synthesis is this: The problem with OO is actually a problem with "inheritance" as the default tool you reach for. Get rid of that and you have what is effectively a different paradigm, with its own cost/benefit tradeoffs.

Inheritance's problem is not that it is "intrinsically" bad, but that it is too big. It is the primary tool for "code reuse" in an inheritance-based language, and it is also the primary tool for "enforcing interfaces" in an inheritance-based language.

However, these two things have no business being bound together like that. Not only do I quite often just want one but not the other, a criticism far more potent than the size of the text in this post making it indicates (this is a huge problem), the binding introduces its own brand new problem, the Liskov Substitution Principle, which in a nutshell is that any subclass must be able to be be fully substituted into any place where the superclass appears and not only "function correctly" but continue to maintain all properties of the superclass. This turns out to be vastly more limiting than most OO programmers realize, and they break it quite casually. And this is unfortunately one of those pernicious errors that doesn't immediately crash the program and blow up, but corrodes not only the code base, but the architecture as you scale up. The architecture tends to develop such that it creates situations where LSP violations are forced. A simple example would be that you need to provide some instance of a deeply-inherited class in order to do some operation, but you need that functionality in a context that can not provide all the promises necessary to have an LSP-compliant class. As a simple example of that, imagine the class requires having some logging functionality but you can't provide it for some reason, but you have to jam it in anyhow.

It is far better to uncouple these two things. Use interfaces/traits/whatever your language calls them that anything can conform to, and use functions for code reuse. Become comfortable with the idea that you may have to provide a "default method" implementation that other implementers may have to explicitly pick up once per data type rather than get "automatically" through a subclass inheritance. In my experience this turns out to happen a lot less than you'd think anyhow, but still, in general, I really suggest being comfortable with the idea that you can provide a lot of functionality through functions and composed objects and don't strain to save users of that code one line of invocation or whatever.

Plus, getting rid of inheritance gets rid of the LSP, which turns out to be a really good thing since almost nobody is thinking about it or honoring it anyhow. I don't mean that as a criticism against programmers, either; it's honestly a rather twitchy principle in real life and in my opinion ignoring it is generally the right answer anyhow, for most people most of the time. But that becomes problematic when you're working in a language that technically, secretly, without most people realizing it, actually requires it for scaling up.

By @dkarl - about 1 month
I think this is really comparing programming without effects to programming with effects. If you want the benefits of using an effects system, you'll have to work a little harder for them. If those benefits don't matter to you, then the extra work is for nothing. The article assumes the second case, and doesn't present it as a trade-off, but only as extra work for the same result, which is misleading.

So how could we compare OOP to FP in a way that evens out this difference? It depends on how you define FP.

You can (like this article seem to) restrict the definition of FP to only purely functional programming, in which a program cannot directly execute side effects, and must return a value representing the effects that the runtime system will then execute. Then an apples-to-apples comparison would compare the FP program with an OOP program that uses an effects system to manage its effects.

How do we do that? Well, if we define FP in a way that forces us to use effects, then the definition excludes a language like Scala, which is essentially a side-effecting OO imperative language that has features that enable FP-style programming. Scala isn't FP by the article's definition, because you can write impure code, but it does let you write programs that manage effects using an effects system. So you can do a reasonably fair comparison that way. I think you would discover that the pain of using effects is the same, if not greater, in an OO language where they have to be added as a framework.

Or you could define FP more broadly to include side-effecting languages like Clojure and F#, and you could compare a side-effecting OO program to a side-effecting FP program. This would be tricky because it would be very difficult to draw a style line between OOP and FP. Would you allow the FP program use OO constructs and the OO program to use FP constructs? If so, you might end up comparing two identical Scala programs. Would you ban the FP program from using OO constructs and ban the OO program from using FP constructs? In that case, you would get an OO program in the style of the 1990s or 2000s, which wouldn't be fair to modern OOP.

I don't think either choice really leads to a meaningful comparison between OOP and FP. I think comparisons have to be more specific to be meaningful, and they have to be in the context of a particular application, so you can fairly compare programs that use effects systems with ones that don't. You can compare Java with Haskell for a particular application. You can compare C# with F# for a particular application. You can compare Scala with an effects system like Cats Effect to Scala without an effects system, again for a particular application. These comparisons are more realistic because you can take into account the pros and cons of using an effects system versus not for the given application.

By @whobre - about 1 month
It’s pretty bad, actually. Especially the Smalltalk/Objective-C flavor with its late binding and messages