Hacker News new | past | comments | ask | show | jobs | submit login
Case against OOP is understated, not overstated (2020) (boxbase.org)
460 points by tejohnso on Feb 10, 2022 | hide | past | favorite | 528 comments



In my mind, state is the real enemy impacting: comprehension, brittleness towards making changes, and the surface area exposed to potential bugs. OOP as frequently implemented, while claiming to encapsulate state, ends up creating so much more.

In accordance with this view, I think project architecture should be approached with an emphasis around how much state is necessary for it to run. This is why simulations like say someone making a game or simcity with like relatively independent entities that map to something in real life use OOP. If you're writing a service doing requests, you want as minimal state as possible. Singletons are state. Initialized/non-static objects are state. The smaller amount you have the easier it is to reason about the system.

As I write this however, I worry a little that my view is overly simplistic, or maybe applicable only to domains that I have worked in. If anyone wouldn't mind poking holes in this argument or offering examples I would appreciate it.


There's a really wonderful talk that I've recommended to almost everyone I've ever worked with called Simple Made Easy[1] by Rich Hickey. I also struggled to explain why I hated state so much. You can talk about races with shared mutable state but even single threaded code I found I couldn't stand it, that it made things harder to reason about and change. It's because state is complex, in the sense Rich discusses in the talk: State intertwines "value" and "time", so that to reason about the value of a piece of state you have to reason about time (like the interleaving of operations that could mutate the state).

I don't know if it's just me but I watched that talk a couple years into my career and it was like something clicked into place in my brain. It changed the way I think about software.

[1] https://www.infoq.com/presentations/Simple-Made-Easy/


The problem I have with talks like this is that they sound fantastic on the surface. They almost sound self-evident! "Duh! I want to make simple things, not easy things! That was great!"

But where are the examples? Not a single example of something easy versus simple, or how something "easy" would resist change or be harder to debug. All of these concepts sound fantastic until you begin to write code. How do I apply it? It's a great notion to carry around, but I often wonder if this is just someone's experience/opinion boiled down to a really well done talk, and not much else.


The point of simple vs easy is they exist on completely different dimensions. There's simple/complex, and there's easy/hard. Something can be simple+easy, simple+hard, complex+easy, or complex+hard. Obviously there's a sliding scale in each dimension.

Simplicity in a vacuum isn't a good thing. Ideally your solution targets the exact level of simplicity vs complexity required for your problem. Obviously you won't always hit or know the target.

The value in simplicity is greater composability. It's especially important for the building blocks of our systems - of which programming languages make a huge portion. It doesn't sound too controversial to say that it's easier to take multiple simple things and make a more complex thing, than it is to take a complex thing and distill it down to the simpler thing you need. I say this because regardless of what programming paradigm you adhere to, the "kitchen sink" unit of code is universally derided, be it god modules or god classes that does shit you don't need.

It's not that Clojure is all simple, all the time. There is mutable state in Clojure - atoms, refs, etc. They also have interfaces. And multimethods. And so on.

But the simplicity floor is lower in Clojure than most other languages I've used. More than those other languages, you can target the level of simplicity you need. And it provides for more complex elements if you need them. And in my experience, a lot of the time, you don't need those more complex elements.


If you want functioning, robust, maintainable software (or even better, software that doesn't require maintenance), then spend a long time modeling the problem domain. Build it as a system of types, a protocol, perhaps even a language (or at least an AST with semantics). Prove things about this model, particularly some useful things about soundness, consistency and (in)completeness. Learn all the funky symbols people use in the literature, learn about the strange tools you weren't told about in undergrad like dependent typing or higher-order contracts or CRDTs and lattices. Spend a lot of time doing this. Then, when you have determined the essential shape of the domain and nothing more, implement the software. At that point, the code almost writes itself.

I submit that if we did that, we would have excellent, elegant, simple software, but following the process would be incredibly hard. So hard, in fact, that it couldn't possibly be distilled into a conference talk.


What sort of domains do you see as sufficiently well-understood and stable where this process is even achievable? A lot of my career has been in domains where we are exploring problems by building and shipping things to see what really works for users and customers. And other times there's domain volatility driven by changes in technology and competitive landscape.

Even for domains that are stable and knowable, I have to wonder what businesses can afford that kind of up-front investment before the first feature ships.


I've had largely the same experience as you, but I have seen some hints that real simplicity could be possible. If the domain is technology itself, there may be no underlying simplicity.

Ultimately, I think we have to make a trade-off between simplicity and easiness. The approach I outlined would be incredibly expensive because the tooling for that approach isn't quite good enough yet, and stakeholders wouldn't even understand it. They wouldn't realize that you were building a pitch for your product not as a PowerPoint deck, but as executable code!

A lot of our complexity today is from constructing software itself over layer upon layer of previous complex software (CSS, I'm looking at you), not due to the intrinsic "business cases" our software is meant to solve. Some of that complexity cannot be avoided, and some of it could be but at significant cost. To use an analogy, it's also cheaper to build a traffic light-controlled intersection, but overpasses are simpler.

Coincidentally, almost all of the tools I've seen that try to make simplicity cheaper come either from the Scheme/Racket/Lisp world that Hickey himself hails from or from Alan Kay and his sphere of influence. (The two groups have quite a bit of overlap, both in terms of ideas and even people.)


Could you please elaborate on Hickey’s and Kay’s key ideas and how to try them hands on?

I know about Smalltalk (Squeak) so I guess that is the playground for Kay’s. Would just playing with Clojure do the same for the Hickey’s?


Sorry, I'm still not seeing how/when the approach you're hinting toward is practically valuable. So far it seems to me like you're pursuing one dimension of quality to the exclusion of others. Which is an interesting theoretical exercise, so if that's your jam, have at it. But it sounded to me like you were proposing something people could actually do.


Compilers maybe?


Ooh, interesting! You're right, there's a class of domain where one can just push the real-world change to the edges of the system and ignore it. E.g., there's surely software that's mainly about complying with laws.

But even there, I suspect adaptation has to happen. Python's had how many versions over the years? Indeed, I could argue that it's one of the world's most successful languages precisely because it keeps responding to user need. Or look at tax software, which is going to change at least every year, and more often in emergencies.

So I suspect at best these other domains have a slower iteration clock. Which might be slow enough for the sort of formal modelling that is described. But then I think there's an open question: do other methods also work just as well with slow iteration clocks?


Speaking as someone with experience with many of those things (PL theory/formal verification background), I don't think they're even close to being a silver bullet.

Coming up with the right abstractions and the right domain model is difficult (especially if you just sit down and try to come up with stuff, you're likely to get it wrong the first time around). Knowing about some of those things could help you come up with better abstractions, but it's neither necessary nor sufficient to ensure that you will.

Take dependent types for example. They allow you to express more program invariants or correctness properties in your types. But actually using them requires you to write proofs (at least, if you're using them to their full potential). And I do think that in general System F like type systems hit a nice sweet spot and are generally good enough for the stuff that you might actually want to handle on the type system level.

I've also run into similar "proof-like" situations with much simpler type systems like those of Haskell and Rust, where I was structuring my types to "make illegal states unrepresentable", but in the process ended up complicating my program due to having to match the structure of my program to the expected structure of the types. Sometimes it is nice to _not_ to have the type system enforce some of your invariants. (Such things are also doable with dependent types of course, but this is just an example of some of the tradeoffs involved).

You can also still have a shitty domain model even if you use all of those fancy tools. They just allow you to be very formal/precise about the domain model (and do perhaps encourage some more uniformity by making it more annoying to express ugly or complicated things).


Domain knowledge is very important. In the real world however by the time you finish this type of process the competition will have had the product out already. It may not be that perfect castle in the sky but it will work and if you have revenue you will have time and means to improve.


Our customers don't even want to pay for something that bespoke. They have margins to worry about.

So instead we've had to make a system which makes it less painful when bugs occur.

For us that means making it trivial to run older major and minor versions our software, and an automated update mechanism which delivers new builds to customers on-premise in less than an hour, updating the DB schema as well.


I don't think this excludes what the GP said, but this is super important as well. I think of it as second-order reliability: design your software not only so that bugs don't occur, but also so that the user can take practical steps to remedy bugs if they do occur.

(Also, as one of my past companies enshrined as an engineering axiom: "write software to be debugged". Most programmers write waaay too few logs. You know the print statements you add to your code when it's buggy, to track down what's going wrong? Well, do that all the time, and if there are too many then fix that problem with adequate tooling. If it's running on your customers' computers - whether servers or PCs or phones - then store them locally for N days / N logs and allow them to be submitted when a bug occurs. Stack traces - even good ones - are not nearly enough.)


100% agree. It's a trade-off. Get product-market fit first and learn what you can about the domain. Spend enough time on architecture up front so you can easily pivot. That's all the simplicity you should care about at that point.

Once you get traction, you can start to afford to have the crazy vision. IMO, at that point it's easily worth the risk. A decent research team will probably discover something, and potentially extremely valuable knowledge.

If you were James Clerk Maxwell before he published his equations, how much would they be worth to you, especially if you had paying customers?


By the time you're 20% in that process your competitor has already overtook the market.


To quote Thiel, "competition is for losers."


Counterpoint is that the Big Design Up-Front utopia didn't win in software, giving rise to Agile (for better or worse).


Easy things work until you have to extend them or do anything the least bit complicated. Think of SQL or most "easy" declarative APIs. Or even worse, ORM engines. Simple things are normally also easy to use, but you may have to write some more boilerplate and there's less "magic".

Steve wrote a simple CRUD API that gets some data and returns it. Bob tried to be clever and write a loosly typed declarative cluster fuck that nobody understands, but it's "easy" if you dont do anything interesting or useful with it.


A bit like haiku, wonderful when you read it, extremely hard to maintain conversations in haiku.

Or like an improv exercise where you have to improvise a dialogue, but only by using questions, no afirmations.

Can it be done? Sure, but not by most people, not in real time. Again, wonderful when you see it done right.


Talking in haiku: Wonderful when you read it. Too hard to maintain.

Improvisation. A constrained dialogue. Affirm? No. Question.

Can it be done? Sure. Most people struggle slowly. When right? Wonderful.


It's easy to stop calling a now-unused function when some behaviour is no longer needed.

The system is made more simple if you remove the function, though.

This is more so if only part of the behaviour of a function is no longer desired - the function becomes easier to understand when it's trimmed down, but it's harder to make that change.


The presenter is Rich Hickey. He is the guy who created Clojure. He basically designed the language around this principle (it is a very opiniated language). If you want examples, look at Clojure and its ecosystem where the ideas of Rich Hickey are held in high regard.


The Clojure language is the example. Basic data structures vs classes/objects, immutable vs mutable, lisp vs other languages, etc.


> They almost sound self-evident!

I think it's hard to provide examples since they would all be implementation dependent.

simple to me is a stage of the thought process that will become apparent only after putting in the extra work. It's not just applying "this 1 trick". Making it simple is its own unique challenge. E.g. my first iteration of an idea is always a mess. Then I rework it enough times to make it presentable (a state where it "works" and I can reason about it with others). But on the job nobody pays me to make things simple because that means spending another 10-30% of the budget on it. making things "simple" at work is nearly impossible to sell because people quickly through arguments at you like "perfect is the enemy of good", and few jobs give you a "definition of done" where making things simple is part of it.

Another reason why it's impossible is that the best time to rewrite a greenfield project or an MVP is before you add additional features. But at that point people will not allow it because the expectation usually is to build on top what you (they) invested in previously.


That time part is what you are wrestling with when you are battling with state. So it's natural to think about it that way. But there's also this somewhat dumbed down version of the argument: every piece of state a method reads is like an additional function argument and every state it writes an additional return value. What a mess.


This is insightful.

In some sense, the only distinction a "pure" function has over "non-pure" is that it declares all its inputs/outputs (as function parameters and result). We say that a non-pure function has "side effects", but all that actually means is that we don't readily see all its inputs/outputs.

Even a function that depends on time could be converted to a pure function which accepts a time parameter - this is conceptually the same as a function which accepts a file, or an HTTP request or anything else from the "outside world".

The trouble, of course, comes from the tendency of the outside world to change outside of our program's control. What do we do when time changes (which is all the time!) or file, or when the HTTP request comes and goes never to be seen again?

Or when the user clicks on something in the UI? Can we politely ask the outside world for the history of all past clicks and then "replay" the UI from scratch? Of course not. We cache the result of all these clicks (and file reads and network communications and database queries...) and call it "state". When the new click comes, we calculate new state based on the previous state and the characteristics of the click itself (e.g. which button was clicked on). This is a form of caching and keeping a cache consistent is hard, no matter what paradigm we choose to implement on top of it.

The real-world example of this would be React. It helps us implement the `UI = f(state)` paradigm beautifully, but doesn't do all that much for the `state` part of that equation which is where the real complexity lies.


There's no such thing as UI = f(state) in React. You may know that already, but it's UI = f(allStatesStartingFromInitialState). That way all state transitions are captured and all state changes are handled accordingly inside components taking into account component's internal state.


This made me think: if we wrote object oriented code methods where all the members that we access are passed explicitly as parameters, as well as all the members that we modify (as out references), then we at least would immediately identify the real complexity of some methods! I'll try to do this, I'm curious to see how that would look like.


> I'll try to do this, I'm curious to see how that would look like.

That looks like a terrible mess.

The problem is not state, but messy access to it.


Everybody agrees that OOP was killed by getters and setters. But I don't think that there is much consensus about how long it would have survived without.

(I'm not saying that OOP doesn't have its place, but it has clearly turned from a way of structuring code to universally strive for into something to avoid if possible)


At some point you get too many parameters, so you pass a struct, which basically means that struct turned into an object. (one interesting difference is that you can pass more than one different struct to that function which is the equivalent of subclassing; but with more permutations possible. Thats actually interesting).


That's not a bad way of putting it. It reminds me of "It is the user who should parameterize procedures, not their creators."


> State intertwines "value" and "time", so that to reason about the value of a piece of state you have to reason about time (like the interleaving of operations that could mutate the state)

Chapter 3 of SICP deals with this topic in great detail.



I think I was at that talk. If I remember right the Sussmans were there as well and Gerry was the first to his feet giving Rich a standing ovation after that talk.


This is one of my favorite talks. It also helped things click for me regarding state. I try to use immutability wherever I can now and when there are unavoidable state changes, I try to understand and constrain the factors that could lead to such a state change. It's simplified things so much for me.


I enjoyed the talk and agree with it in many ways, but perhaps a contrarian stance will stimulate some interesting discussion. Here's the steelman I can think of against that talk.

Hickey's fundamental contention is that whether something is easy is an extrinsic property whereas whether something is simple is an intrinsic property. Whether something is easy is dictated often by whether it is familiar, whereas simplicity lends us the more ultimately useful property of being understandable.

To which I'll counter with Von Neumann's famous quote about mathematics : "You don't understand things [simple]. You just get used to them [easy]."

There is no fundamental difference between ease and simplicity. Simplicity (of finite systems) is ultimately a function of familiarity. There's a formal version of this argument (which is effectively that most properties of Kolgomorov complexity when applied to finite strings are defined by your choice of complexity function, even in the presence of an asymptotically optimal universal language. In particular there is not a unique asymptotically optimal universal language, that is the Invariance Theorem is overhyped), but the informal version is that both simplicity and easiness arise from familiarity.

Indeed the fact that there is "ramp-up" speed for simplicity suggests that in fact what is going on is familiarity. E.g. splitting state into "value" and "time" is one way of thinking about it. But I could easily claim that in fact "time" complects "cause" and "state." Rather state machines where the essential primitives are "cause" and "effect" are the proper foundations from which "value" and "time" then flow (you can think of "effect" nondeterministically, a la infinite universes, and then "value" and "time" fall out as a way of identifying a single path among a set of infinite universes). Likewise Hickey claims that syntax mixes together "meaning" and "order" whereas I would could just as easily say that "order" complects syntax and semantics!

What of the idea of "being bogged down?" That "simple" systems allow you to continue composing and building whereas merely "easy" systems collapse and are impossible to make progress on past a certain threshold? I claim that these are not intrinsic properties of a system. They are rather extrinsic properties that demonstrate that the system no longer aligns well with the mental organization of a human programmer. However this is dependent on the human! A different human might have no problem scaling it.

Now hold on, perhaps, while simplicity is perhaps dependent on the human mind and humans all more or less have the same mental faculties. Perhaps we can't find a truly intrinsic property that we call simplicity, but perhaps there's one that's "intrinsic enough" and relies only on the mental faculties common to all humans. That is, returning to the idea of "being bogged down," there are systems whose complexity puts them beyond the reach of all, or at least most, humans. We can then use that as our differentiator between "simple" and "easy."

To which I would reply that this is probably true in broad strokes. There are probably systems which are are so arcane as to be un-understandable by any human even after a lifetime of study. But at a more specific level, the way humans think is very varied. The ways we learn, the ways we develop are hugely different from person to person. Hence I find this criteria of "bogging down" far too weak to support Hickey's more concrete theses, e.g. that queues are simpler than loops or folds.

When you're talking about things like love, hate, and fear, sure maybe those are universal enough among humans to be called "objective" or to have associated "intrinsic properties," but when you're talking about whether a programming language should have a built-in switch statement, I don't buy it.

For the purposes of programming languages, simple is not made easy. Simple is easy. Easy is simple. The search for the Platonic ideal of software, one that relies on a notion of intrinsic simplicity, is a false god. Code is an artifact made for consumption by humans and execution by machines and therefore any measure of its quality must be extrinsic to the humans that consume it.

Sometimes X is simple. Sometimes it's not. It all depends on the person.

As empirical evidence of this I leave this final exchange between Alan Kay and Rich Hickey where the two keep talking past each other, no matter how simple their own system is: https://news.ycombinator.com/item?id=11945722


I appreciate the thought process here, and I'd want to spend more time thinking it over before a full response - though I think it maybe goes a little bit too into etymology for my taste! My immediate comment is that working memory is a measurable finite resource that developers have to use. The more entities they have to track in order to model the part of the system they're working on, the more usage of working memory.

Every bit of state creates potentially exponentially more possible entity states. So therefore limiting potential changes in state limits the amount of working memory necessary to understand the system. Its starting with "can't" and then building a "can" when necessary, which is a lot better on memory, comprehension and feeling safe/secure to make changes then starting with a collection of 10^n "can"'s and adding in "can't"'s.


First off I don't think this is quite the way Hickey thinks about the issue (though I suspect he would agree about the working memory part), especially with the comment about etymology /s!(it's a meme in Clojureland that every Hickey presentation and library must contain at least one slide on/mention of etymology) In particular Clojure as a whole embraces an ideology of "open systems" vs "closed systems" where we start with an infinite sea of "can"s and then add "can't"s as needed.

But that's immaterial to your main point, which is that adding state into the mix of things makes things hard. Which I agree with, but again to steelman the point, I could turn around and say that values allow for exponentially more possible values as well! When I see a map passed into a Clojure function I have no idea what could be in that map!

I think the main objection here which you are alluding to is one of "global" vs "local" reasoning. With a value I just need to worry about the body of my function, whereas with (global) state I need to worry about every function everywhere! But what if that's just a problem with our tools rather than an intrinsic issue? What if I had a tool that could automatically present all the mutable state of your system that is publicly accessible as a single screen and automatically link to different procedures that link to different parts of it? At that point I don't see much of a difference between state strewn everywhere and nice orderly values plumbed everywhere. In fact maybe it's nicer to have that implicit state strewn everywhere instead of having to carry around values which are irrelevant for the bulk of a function body and only relevant for a single part of a subfunction. What if it's all just a matter of not having the right IDE?

Working memory is definitely a hard limitation and universal enough among humans, but it's not clear to me it's a specific enough concern to convincingly justify certain programming language features which may just be crutches for inadequate visualizations or different educational backgrounds.


> But what if that's just a problem with our tools rather than an intrinsic issue? What if I had a tool that could automatically present all the mutable state of your system that is publicly accessible as a single screen and automatically link to different procedures that link to different parts of it?

The world needs this. I think Pernosco has a workable technical foundation, but the GUI is a debugger and I need a code exploration tool to "find my way" in big unfamiliar codebases. Encouraging developers to pick up and hack around in others' codebases is the only way to get enough eyeballs to make all bugs shallow.

> maybe it's nicer to have that implicit state strewn everywhere instead of having to carry around values which are irrelevant for the bulk of a function body and only relevant for a single part of a subfunction.

I think global state (which is unusually bad) or shared mutable state (which is omnipresent outside of Rust) is a mental overhead (more things to keep in mind). I don't think tooling can eliminate the overhead of worrying about moving parts, only make it faster to look up (and hopefully document) what touches each bit of state.


There's a lot to think about in your comments in this thread but I have a nitpick about functional programming style here.

> In fact maybe it's nicer to have that implicit state strewn everywhere instead of having to carry around values which are irrelevant for the bulk of a function body and only relevant for a single part of a subfunction.

I would call this an anti-pattern in FP. It's often a symptom of trying to replicate more imperative styles like OOP in a pure language. Threading mostly-irrelevant state through a bunch of different functions is a sign that your program is under-abstracted. If you think of all the function calls in your functional application as a tree, state should stay as close to the root of the tree as possible, kept in nodes it's relevant to, and the children and especially leaves of these nodes should be decoupled from it to the greatest extent possible.


> Threading mostly-irrelevant state through a bunch of different functions is a sign that your program is under-abstracted.

The problem is that often you do want fairly complex state in the leaves of the tree, but want very little of it in anything else. Web browsers are a classic example of this. Pure FP solutions such as Elm that completely eschew the idea of local mutable state require a lot more ceremony to implement something like a form (the classic thorn for Elm users). By forcibly moving up the state to the root, you sometimes end up needing to pull some fairly severe contortions.

E.g. the usual answer to move the state back up to the root in the land of statically-typed, pure FP is to express it in a return type (e.g. a reader or state monad, culminating in the famous ReaderT handler strategy in Haskell) or in the limit bolt on an effect system instead. The usual answer in impure FP is to accept some amount of mutable state and just rely on programmers not to "overdo" it.

But from a certain point of view, writing an elaborate effect system whose very elaborateness might cause performance issues and inscrutable error messages sounds suspiciously like trying to work around a problem in visualization with an over-engineered code solution. And from another perspective it feels a bit like a trick. If some function has a lot of state, then I would hope by opening up the definition of the function I'd see how it all works, but with an effect system all of a sudden I've split things up into an interpreter that actually performs the mutation and an interface that merely marks what mutation is to be done. It feels like I've strewn logic around in even more places than if I just had direct stateful, mutable calls there!


I will say plainly that I think there are situations in which mutability offers more elegant solutions than immutability, but I think most languages that offer it do it badly. I’m most experienced programming the Erlang platform via Elixir, and I think it offers a really nice midpoint between locality of state and purity. Within a process everything is immutable, and mutation requires sending a message to a process that will have a function specifying an explicit, pure state transformation from that message. Just about the only thing I don’t love about Elixir is the lack of real types.

I’m also very pragmatic and to the example of a web browser I would say, most applications are not web browsers. The overwhelming majority aren’t, in fact. I’ve chosen at this point in my career to mostly focus on enterprise software development, which I believe was Rich’s original field as well, and I’ve seen an enormous number of solutions with too much state cast about everywhere that benefit massively from centralizing the state high in the tree and really thinking through the data model carefully. So I stand by the principle I advocated originally, but it’s not universally applicable. It’s my belief that one of the core virtues of software development is knowing when to apply which principles.


> to the example of a web browser I would say, most applications are not web browsers.

I should've clarified. I meant developing a web page to run on a web browser, hence the form example.


It’s a good point. UI is a situation where the classic OOP-style frameworks work really well when they’re carefully designed. I think we’re still waiting on a model for doing that with FP that doesn’t rely on passing state deep down into an expression tree like React and its descendants encourage you to do. There’s stuff like Redux but it has its own problems.


You can "solve" global mutable state with an IDE until you bring concurrency plus parallelism into the mix. Then all bets are off for mutable global state.

In the case of Clojure, the map that you pass to a function is a value. It is guaranteed not to change underneath you and it can be freely shared with anybody.


Well to keep my contrarian hat on...

> concurrency plus parallelism into the mix

The hard part of concurrency is writing or writing+reading, not just reading, so an immutable map isn't going to solve everything. Instead the hope is that you confine the mutability to one place with various transactional guarantees (in Clojure's case, this is usually atoms) and then everywhere else you don't have to worry about it.

But then again why couldn't the same analysis be performed on mutable state? How are we sure this isn't just a tooling issue? If we knew exactly what parts of mutable state were being touched by what we could identify what critical sections needed various guards.

Taking my hat off and going back closer to my own views, I actually think Clojure's combo of maps+atoms are an arguable case where Clojure has in fact complected things together in a way that e.g. STM doesn't (and Clojure's implementation and use of STM has its own problems). Namely it's complected committing a transaction with modifying an element in a transaction.

To illustrate the problem, right now Clojure atoms basically give up parallelism entirely. If you have a map in an atom with two threads modifying different keys, then those threads have to come one after another. It's actually kind of a waste of resources compared to the single thread case because work done in one thread will be thrown away and retried if the other thread wins.

So if you want true parallelism when modifying different keys you can use a ConcurrentHashmap. But that then gives up atomic updates of multiple keys at once! (Or you can have nested atoms but that has its own problems and doesn't solve the inter-key atomicity issue).

It looks like an all or nothing proposition where you either get non-parallel but fully atomic map updates or parallel per-key updates but nothing in-between. These kinds of false dilemmas are a classic symptom of complection.

The way other languages with an STM system deal with this is to build concurrent maps out of STMs refs. That way you get exactly the amount of parallelism you can relative to the amount of atomicity you need. If you have a transaction that touches two keys at once then both of those keys are atomically updated together and those two keys form one unit of parallelism. If you have a transaction that only touches one key then you have per-key parallelism. If you have a transaction that touches all the keys at once then you just collapse to the normal case of a map inside an atom.

As far as I can tell the reason Clojure doesn't do this (but other languages have) is that its STM API is a bit clunky and missing some interesting combinators.

All this is to say that maybe indeed simplicity and ease aren't all that different if from one perspective atoms are simple and from another merely easy.


Those are well reasoned points.

I'm not going to delve into STM because that can be a whole book worth of discussion :). It's a fascinating universe, I've spent many hours (weeks, months?) exploring it, and I don't consider myself even close to an expert.

You are absolutely correct about the trade-off about atoms in Clojure.

Practically speaking, to start seeing retries you'd have to have a big number of updates going on at the same time. You can push a huge number of updates through a single thread. If you do have the need to do big throughput, you can explore not-so-idiomatic options like atoms-in-atoms, like you said.

IMO, the biggest unique benefit of combining atoms with immutable persistent data structures, comes from the fact that you can get unlimited number of consistent readers virtually for free. Any thread can look at (aka, deref) an atom, while the state/world keeps moving forward. I don't think any amount of tooling can solve that case for mutable data. A snapshot of a mutable data structure would require copying the whole data structure while using some sort of a locking strategy to stop writers while the read is taking place.


In production, I may only want one connection pool to a DB, and in that case global state is pretty much equivalent to passing state as an argument. Development in a Clojure REPL is a different story. I have one connection pool for the dev server, and a separate pool to run tests against. The test db is re-created from a template between each test run, without affecting the dev db at all. I can trivially have multiple test pools if I want to run tests concurrently.

I also have a separate service that the server makes calls to, which doesn't run on this server in production (it has its own production server), but does run in dev and test. Each dev/test system runs a separate instance of this service, which has its own separate connection pool(s), and setting this up was trivial.

Needless to say, failures are reproducible and meaningful. There is no mocking -- we test against real local services with real local DBs. (There are still some remote service calls which I'm slowly replacing, and some flakey, unavoidable remote dependencies in a few browser tests).

I didn't do anything special to make this possible other than naming the config files "service-name-config" instead of just "config". It is just the natural result of passing state in explicit arguments. The same is not true of global state.


To continue with my devil's advocacy...

> It is just the natural result of passing state as explicit arguments.

But nothing you've mentioned here is intrinsic to mutable state. It seems like all that's happened is you identified a part of your program that you wanted to be configurable and exposed a configuration knob. If for example you wanted to make it so that there is a test mode that where you want to prefix "test-" to every string written to the DB that would also probably involve a new argument somewhere. There's nothing here special about the mutable state part of it.


> To which I'll counter with Von Neumann's famous quote about mathematics

I’m fairly sure this great quote is about mathematical “objects” in that you will never be able to truly “understand” or have a “real feeling” for more complex ones, like higher dimensions. Yet, by applying some simpler rules we can use and transform them, and after a bit of practice that will make it feel “close to us”, or “real”.

> Simplicity (of finite systems) is ultimately a function of familiarity.

I really don’t believe it would be true. Maybe I’m misunderstanding, but no matter how familiar I am with a given crud program vs JIT compiler technology, the latter will always be complex - but as you later refer to, I’m sure you know the difference between essential and accidental complexity. But in this view I would rather say that simple things are ones with minimal accidental complexity, while the easy-hard axis is about the essential part of that, that is irreducible.


>>> the way humans think is very varied

>>> It all depends on the person.

Based on what I've recently learned about neuroscience and optogenetics, I don't think there's much evidence to support this sort of relativism. On the contrary, many processes in mammalian brains have common mechanisms.

To explore more, this is a great podcast https://peterattiamd.com/karldeisseroth/

Disclaimer: I am a complete layman on the topic, so please correct me if I'm wrong.


There is more to how we think than the underlying mechanisms, just as varying programs can be run on the same hardware.


This concept of "used to" vs "understand" reminds me of an interview with Feynman where IIRC he explains how can magnetism work at a distance to a layman person. He discusses about the "why" questions and how you keep getting deeper and deeper each time you ask "why". He concludes that his explanations won’t be satisfying for the other person, saying "I can’t explain this to you in terms you are more familiar with". I thought it was interesting and related. I’ll try to find that video.


It's the Feynamn "Fun to Imagine" video / series.

This bit is wher ehe says that about magnets: https://youtu.be/P1ww1IXRfTA?t=1300


I want to add to this that physics aims at this 'simplicity', i.e. being able to derive mathematical models ab initio, with the least amount of assumptions.

While the 'simplest' (in the physics sense) description of something is elegant, it can also be extremely hard to understand and work with. Maxwell's equations are used in engineering for a reason - and not their simpler theoretical physics underpinnings.


If you're going to reference a Rich Hickey take-down of OOP, I think "Are We There Yet?" is the most pertinent: https://www.youtube.com/watch?v=ScEPu1cs4l0

Of course, Simple Made Easy is excellent too, probably his most influential talk.


Time does not go away from the concept of value when you remove state.

What state takes away is access to a given value at any other time but now.

It's always now; every value is the current value and no other version of that value exists.


Not just you, I had the same experience. I rewatched it several times over the years and understood something new every time.


> State intertwines "value" and "time"

Reminds me of deterministic finite automaton. Is that what you mean?


Me as well but I was already sold on Clojure by then.


> ends up creating so much more.

This is primarily because of inheritance, which seems counter-intuitive. In a meta-analysis of OOP-based designs, inheritance is used as the primary form of composition with other strategies being either last-resort or added later when the inheritance is already deeply embedded as part of the design.

Inheritance is a brittle form of a composition (no-reinherit) that nests state in a deep tree-like type system, rather than isolating it into attachable modules. Most OOP-based languages have slowly had to adopt additional forms of composition, as inheritance is not suited well for cross cutting concerns. Ironically, almost anything added after the base class (and maybe some abstracts above that) is a cross cutting concern added after the core functionality is established.


> In a meta-analysis of OOP-based designs, inheritance is used as the primary form of composition

Whose meta-analysis came up with that? Like to see that.

> Ironically, almost anything added after the base class (and maybe some abstracts above that) is a cross cutting concern added after the core functionality is established.

That's a bold statement (unless you have a novel definition of "cross cutting concerns") and actually backwards: The super provides the generalization and subs specialize. A cross cutting concern is a 'general' concern. AFAIK, cross cutting concern is a term originated by the inventor of AOP, and the typical garden variety CCC deals with matters that rarely have anything to do with the types to which it is applied. (Debug log in-args is a garden variety example.)


> Whose meta-analysis came up with that? Like to see that.

You'll have to dig into each language that has expanded it's composition capability and the reasoning, but the outcome is self-evident. Many languages started with simple inheritance (eg PHP, Java, VB, C++, et al) and expanded composability mechanisms over time.

> That's a bold statement (unless you have a novel definition of "cross cutting concerns") and actually backwards: ... A cross cutting concern is a 'general' concern.

I'm not going to argue about how you wish to redefine things.

Good luck with whatever.


> This is primarily because of inheritance, which seems counter-intuitive

I agree that inheritance creates a lot more problems, but the usages of non-static methods and internal state even in classes with no usage of inheritance can feel just as bad, when you have a high level method utilizing instantiated objects. Internal state as a whole can be avoided fairly often


I don’t see much difference between

    some_state.do_stuff()
    do_stuff(some_state)


They're just different syntaxes for the same thing. I think what OP is driving at isn't the syntactic difference but making immutable what doesn't need to be mutable. You could do that with either syntax.


Yes, but I think the person he/she was replying to is right, the biggest problem is inheritance.


The second one scares me. It implies some_state is mutated (or not, we may just be logging something) by the do_stuff function while the first makes it very clear that some_state is in charge of doing stuff and that the implementation is aware of how some_state is implemented itself.

OTOH, the second one would be much better (and imply immutability) if it were written as

  new_state = do_stuff(some_state)
But it'd also allocate a new state.


This is a symtom of seeing everything with the OOP and state glasses.

let's have do_stuff=square and some_state=2.

What does square(2) imply?


Better yet, what does 2.square() imply.


Immutability is independent of whether something is a method call vs a function call.


I would say a big contributor is also reference semantics for classes being the default behaviour in many languages. You end up sharing the state and increasing the surface area of your code which can touch the state with every pass-by-reference in the code base

I know there are mechanisms to avoid this, but many times they are opt-in rather than opt-out, and so it encourages this access-to-state propagation through the codebase where you have far reaching consequences


If only we could completely eliminate state! Thankfully, I am working on a plan for this. It should take around 10^106 years... give or take.

The serious comment here is that the real world imposes a minimum floor on the amount of mutable state that you have to model. Databases are giant piles of mutable state. Maybe we should start talking about "essential state" and "accidental state" the way we talk about complexity.


I agree and would add that the UI is also a pile of mutable state. Even if you model the DOM using a pure function, there's still scroll position, selection, animation, history, and so on.

At the end of the day, users interact with state. We need languages and techniques that manage it well.


Yes. This is one of the core problems of FP style analysis of what's "wrong" with software development. Sometimes those people act like state is some sort of bad habit, like chewing tobacco. But the way they say to get rid of state really just hides it behind large state-management engines, like browsers and databases. It boils down to "state for thee but not for me". Well, great. But some of us have to deal with the fact that computers are often used to model the real world, which isn't a pure function.


There is a common pattern in OOP land that you use ooo.setXXX(yyy) to dupe states across objects/fields. Instead of using some kind of getters to map them. (probably due to difficulty in languages to link between objects?)

You end up get some states that should just the same but in multi places.

And this is almost one of a biggest source of bugs. Because you WILL do it wrong. As the code grows, the place you need to manual synchronize data grows. You end up miss it, create a lot of bugs.

On the other end, in most FP languages. It is pointless to dupe state most of the time, so this kind of problems did not happen so much in the first place.

BTW: Personally I like the idea of the [computed](https://v3.cn.vuejs.org/api/computed-watch-api.html#computed) primitive of vue, because it make the getter/setters first party encouraged. And getter/setters never add states. If you use it properly, states can be reduced by a lot and with much shorter code. While it looks the same as manually dupe them on surface, so you don't need refactor everything just to use it.


I like the term accidental state. This is also the type of state you see in a lot of OOP code, as referred to by parent.

Beginners want to keep functions short and the way to do that is to chunk up a bigger method into several smaller, then realize oops, that you needed that variable in both functions. Store it into “this” and now instead of one decoupled function you have two coupled functions.

Contrived example written on phone but code like below is extremely common, especially from Java coders who have been mislead to make classes for everything and haven’t learned the static keyword yet. Here obviously the self.stuff is the accidental state creating coupling between the functions, that now carefully have to be called in correct order and any of their mutations to self can impact the other.

    class Worker:
    def init()
        self.setup()
        self.foo()
        self.bar()
    def foo()
        self.stuff = fluff
    def bar():   
        do_work(self.stuff)
Rather than just do_work(bar(foo(fluff))).


I agree with this - and of course without any state whatsoever the program is unlikely to be useful. A database, a network connection pool, and initialized configurations are required pieces of state for just about any backend service, and you can't really get rid of them. But having clear lines around how state is stored and utilized and minimizing it in business logic to me creates a much more sane program.


Isn’t exactly these boundaries what OOP gives us?


This is true, and another thing about state is also true - relational databases are much better suited to handle state cleanly and to minimize the amount of it than programming languages (except maybe prolog). Especially OOP languages are bad at state minimalization. For example there's no commonly used equivalent to normal forms in oo. There are no indexes and no materialized views.

The funny effect is - if you want to minimize state and you're serious about it - keep everything you can in database and make a 2-layered (relatively thin) client <-> relational db architecture. With stored procedures. We were there in 90s and we moved away because web and OOP became fashionable.

So we took our clean, normalized, minimal state from db and made it messy and complicated with ORMs to satisfy OOP gurus :)


I don’t think ORMs are fair choice as prototypical examples of (good) OOP. But I agree that having data (record) primitives is very important in some given domains (and that relational databases are really powerful). There are other cases as well which are not as data-oriented though.


ORMs were the OOP way in 00s and 10s.


People also moved away from that paradigm because databases are slow. I work in the world of optimizing TLP level communications over PCI-e buses. To me, a database access is already in the world of "why bother?".


We moved from

    DB <-> Front end
to

    DB <-(ORM)-> APP SERVER <-> FRONT END
I don't think it's any faster :)


Simple reason - add local or distributed cache to the app server, scale it along front end horizontally and you can handle several orders of magnitude more traffic.


>project architecture should be approached with an emphasis around how much state is necessary for it to run. This is why simulations like say someone making a game or simcity with like relatively independent entities that map to something in real life use OOP.

In the beginning of my career I did a lot of engineering simulations (Simulink), to me signal flow diagrams have always been a very obvious way to model programs. All the state is explicit in that it becomes a delayed output->input mapping of the signal flow graph. Each block behaves the same, because it has no internal state.

I always thought about programs in the same way. What goes in, what goes out, what goes back in (if multiple iterations). Only later did I find out about functional programming, which basically is the same idea, and that instantly clicked.

Except for the auto-completion after writing the . on an object, I've never really seen OOP (in the Java way, not the Erlang way) be intuitive or simple. Always keeping state in mind, class hierarchies spanning tens of files where the only way to know what your object really does is step through with a debugger, interfaces for everything because otherwise you can't mock the classes for the test, the list goes on.


I think in a lot of ways you're correct. FP and imperative code tends to makes state explicit; OOP hides state. The latter MAY make things "easy"; it never makes it simple.


To continue my hot take from earlier, OO wasn't supposed to "hide" it anymore than FP was supposed to. Rather, OO was supposed to be about changing the metaphor of the program as you are writing it.

This is most easily seen if you consider a TON of the domain specific languages out there. Logo, PostScript, DVI, GCode, etc. Many of these are "move to X" "put down pen", "move to Y", "pick up pen", etc. Very imperative and how you would talk to someone on how to do something.

So, if your objects give meaningful verbs to control the state that they maintain, it works rather naturally to reduce the code that you have.

Now, most OO today, that I see, embraced objects as records. And goes out of its way to not encode any language of behavior in the code they let you write. But, I don't think that is enough of an argument to say that abstracting some active objects into an OO paradigm is a waste and can never help.


I think you are exactly right. There is a huge difference between data-oriented and behavior-oriented parts of a program. OOP is only a great tool for the latter but it does allow for immutability besides it, it is not either-or.

Wrap the behavior in classes and have data as data. Hopefully this will be indeed the direction taken by Java with its records and other new TBD features.


"Hiding" state is necessary to endow it with well-defined invariants. This can be done in many FP languages, too. The semantics-side implications of "encapsulated" state w/ proper invariants have yet to be explored, though, and this is where newer PL formalisms like "homotopy types" might end up being quite helpful.


> The semantics-side implications of "encapsulated" state w/ proper invariants have yet to be explored, though

It seems that that's called a state machine, and OOP objects should come with state charts, but they don't.

> and this is where newer PL formalisms like "homotopy types" might end up being quite helpful.

PL research would actually get adopted if they didn't insist on using the worst possible names for everything. If they're not calling something an "intuitionistic type theory in the calculus of constructions" they're calling it a "pi-calculus".


> It seems that that's called a state machine, and OOP objects should come with state charts, but they don't.

That is because the state chart would so quickly explode into uncountable states or so difficult to understand transitions from state to state, that such a diagram would become instantly useless. Which only goes to show, how unrealistic the idea is, that you can really fully understand such a system and that shows what the problem is. Granted, there may be certain areas of related state, that are separated from other areas, but when the program becomes non-trivial, the lines usually blur, unless some kind of approach is used, which reminds me very much of FP, only that it wraps functions uselessly in classes and objects, instead of making use of modules and functions only.

In an FP style, ideally each function would be a thing you can look at separated from the whole system, if you know what its input can be (which may be difficult). That makes for testable code. I should be able to test every function separately, without having to use ten other classes to make instances to set up an environment, in which I just hope that what I wanted to test, can actually be tested.


> In an FP style, ideally each function would be a thing you can look at separated from the whole system

That is theoretically impossible - complexity is fundamentally not decomposable into parts in the general case. That is, given a complex function, you may not be able to extract one more meaningfully separate part, leaving the core function still too complex.

OOP’s general idea is to encapsulate just enough of the complexity to make it possible to reason about its outside API, while the complexity will live inside, allowing the class to enforce some of its invariants.

Don’t get me wrong, I’m not saying that FP is bad, hell, I think that both paradigms are essential, they are not either-or choices.


I think you are slightly misunderstanding me or I did not put it very clearly. Of course there will be complexity inside functions in an FP style. I think I never said there would not be.

What I want to express is, that I can call every function of the program separately. I might have to put effort into preparing the call's arguments, of course, but I can in the end look at its inputs and outputs in a unit test, separate from the setup of an environment. The environment is basically in the arguments of the function.

The FP paradigm encourages people to avoid global state and state mutation, which helps with reducing the setup effort required to make the arguments for the function call.

In an OOP style program, I cannot simply call and test every part separately. I will have to create a kind of landscape of objects, which experienced the set of state mutations, which hopefully sets up an environment, in which I can test for one specific case of a method doing what it should do in that case. That is the moment, when the state diagram has already exploded into uncountable states, usually impossible to keep all in your head. It might also be the case, that the constructor of an object interferes with the actual setup, that you want to have. Then you will need to apply mutations to change the state to get there, doing more work than ideally would be necessary.

I see OOP maybe still in things like GUI. People are trying to get declarative there as well or functional, but there it seems like a normal thing to have some widget really change state, to avoid overhead of creating a new widget and re-displaying it. But maybe in the future FP will invade this territory as well somehow.

And yes, you can combine FP and OOP, but many common practices used in OOP are detrimental to the advantages FP can bring. I thing it would be best to limit OOP to parts of the system, where it makes sense and then wrap it in an API, which protects the rest of the system from having to use mutation all the time. The question becomes again "What is OOP?". Is it still OOP, if I work with structs and functions working on structs, instead of objects? Do we use Alan Kay's definition with message passing and each object being its own little machine? I think in Erlang we have some combination of it. Like Joe Armstrong said in a talk with Alan Kay, it is either the most or the least OO lang. Well maybe nowadays we have different candidates for that as well.


> if they didn't insist on using the worst possible names for everything

If the alternative is stuff like "FactoryFactoryFactory", I'm not sure that's better.


It might not be better, but at least i don't need a dictionary to understand what it is all about, I just need to read it again and sort out the words in my head.


Absolutely. Why make const char * data private in a string class otherwise? I have know it's in a valid state so I can get it to the next valid state when caller runs an operation on it.

But then a lot of proving (small p prove) an object in c++ is in a valid state amounts to hoare predicates and other spark-ada like expressions.

Certainly FP could do same and like c++ define that away when callers and callees think undefined behavior is gone?


What would homotopy types bring to the table?


They seem to be necessary if you want a notion of "equivalence" (for both values and types) that enables you to make functions, operations, constructs etc. independent of any notion of "underlying representation" as well as seamlessly applicable across equivalent 'representations'. This is desirable in both higher mathematics (where homotopy types were first developed) and software engineering, for much the same reasons.


Could you explain this to a layman with an interest in FP?


I read about this a while ago, I'm not an expert but this is my take on it: In homotopy type theory an equivalence of types is a first class value that you can manipulate. And also you can separate types from their 'implementations'. Classic example is you have a Nat type, with a Peano construction (Nat is a zero or a successor of a Nat.) This is not very efficient, but you write functions with it, prove things etc. It's time to optimize, and you change your Nat implementation to something more efficient (e.g. a Nat is Zero, or twice a Nat, or twice a Nat + 1). Your functions and proofs that you wrote with the previous implementation still will work and your type signatures won't have to change


Isn't this what a homotopy category is for though?


Sorry, but state is everything. If you don’t have state, then you’re essentially doing useless work computing an answer that is already known. Computation is only useful because of state.


Here's a pure computation:

    import Data.List (nubBy)

    refuteGoldbach :: Integer
    refuteGoldbach = head $ [ n
                            | n <- [4,6..]
                            , not $ n `elem` [ p1 + p2 | p1 <- primesTo n, p2 <- primesTo n ]
                            ]
      where primesTo n = takeWhile (< n) $ nubBy isMultiple [2..]
            isMultiple m n = n `rem` m == 0
If you think you already know the answer to this computation, get yourself a Field's medal.

And then there are pure functions. Every time you compute a function using an input no-one has tried before, you are probably computing something that is not already known. You do this routinely even with a calculator.


And if you don't store the answer in some kind of state, it's lost and computing the pure function was useless.


In typical functional languages state is managed so that it doesn't hurt, not completely absent.

For example, a REPL keeps state in the messages it prints to the terminal and this state isn't visible in the pure function definitions and expression the program is concerned with.


You don't have to throw away the result. That's what the State Monads are for. Immutable State is Ok. Memoization is Ok.


> get yourself a Field's medal.

Unfortunately I'm over 40, otherwise I would try


This is a really poor take and you know it. State can exist in functional systems - as results of computations passed to other computations. Recursion where the new argument is the state.

No one was saying "don't use state" they were saying we need to adjust how we use it.


This is what I don't understand about the mutable vs immutable, my functions only exist to mutate state.


> only exist to mutate state

Which part of the state, and when?

Is this your program?

   // change anything, anywhere, in the entire database, the biggest state
   execute_sql(user_input)
You might want controls around that state so not anyone can change it. It might be read only for some users - that is, immutable.

Is this your program?

    log_to_database(financial_event for $5.00)
You probably want your financial logs to be immutable. Nobody should be able to mutate that $5.00 event to be $500.00.

Is this your program?

    o.foo = complex_function(...)
    ... 1000 lines later ...
    o.foo = null
    ... 1000 lines later in another file ...
    o.foo.do_stuff() // oops! foo was set to null somehow - but where?
    
The above scenario has shared state with "foo". Somewhere it was set to null somewhere. It could be set to null anywhere in your program. Good luck tracking the bug. If "foo" was immutable, you would know immediately where null came from, because it can only be initialized one time. It lowers the cognitive load, knowing that certain actions are impossible makes it easier to focus on what matters.

Is this your program?

    thing = computation()
    return thing + 2
Many program are a series of computations - fresh state is created on each line, nothing is mutated. There is no reason not to be using immutability. In fact, if immutability were throughout, a compiler wouldn't have to worry about things like aliasing.

This is all the tip of the iceberg, there are many reasons to enjoy using immutability.

https://en.wikipedia.org/wiki/Aliasing_(computing)


but, in your foo example, presumably complex_function() returned some data that was important, and foo was set to null for a reason, so that when you call do_stuff() you need to know was it supposed to be called on using the result of complex_function, or should it not be run at all because there is a missing null check.

I guess what you are saying is that foo should not have been changed, but a new variable created called "can_do_stuff" and it should have been checked before the call do_stuff()

I think the old Carmack article linked somewhere below makes a very good case for why pure functions are a good thing, and I see the value of making small atomic changes to an apps state, but ultimately, almost every applications job is to mutate data.


>Is this your program?

You sir - should write flyers and product-copy :)


Try instead this for pure function: thread safe radix trie supporting ins, del, find. Purpose exclude blacklisted ip addresses. Go!


This question was not snark for FP pure functions or otherwise; no ill-will! It needs a legit answer maybe with a small lecture on why radix trees can be FP not pure FP. Hey, I can read and learn.

Other sorts of streaming operations --- message in, transform to new object which is logged or put into a data store most certainly do not need mutation so long as they are cache friendly, performant. FP and friends might even argue: yah ok it's some 10 pct slower but no race conditions, no weird sync. Formal methods are easier there to deploy. Those secondary arguments can be effective too; I'd bite


With mutable state, your function mutates state somewhere in the environment (typically a global variable or the class in which the function resides). With immutable state, your function returns an immutable value, never modifying anything. I program Java for the most part, and this is how I do most things. Mutable state is the enemy, not OOP. Some people think mutable state and OOP are inseparable but they are just looking at a definition of it that is easy to criticize. If you make every object immutable , OOP actually becomes a much nicer model to work with.


I have come to the same conclusion. State is the problem. State should be:

- minimal (amount and lifetime)

- well conceptualized (~= easy to understand the organization)

- well named

- minimally exposed

- coherent by construction (make inconsistency impossible by design of the format or by offering updating functions that ensure the invariants)

OOP can actually help with some of these things! I develop mainly in C++, which doesn't encourage a purely OOP style like Java.


I like your "bullet points" and agree with them all.

Wat are your thoughts on (super simplified example):

* 1 state-var with 3 values ?

* 2 state-vars with 2 values each ?

Sometimes I steer my design too much to first example and then other times to the last example. Both extremes can make things ugly


This is a typical conflict, and I think my main problem is that I spend too much time worrying about it. The important thing is that you make sure that they cannot become inconsistent (you can do this by always going through a function that ensures that when updating them). A thing I have done somewhat recently is:

  enum AuthConnectionState 
  {
      WaitingForConfig = 0,
      Disabled,
      Connecting,
      Connected,
      TimedOut
  };
where the value of the corresponding variable is derived (in just one place that is called when any input changes!) from many inputs, and it's the authoritative source of information. If you want to know whether the current state allows to proceed with login (which can be local if so configured or the connection definitely failed), call:

  static bool connectionStateAllowsLogin(AuthConnectionState state)
  {
      return state == Disabled || state == Connected || state == TimedOut;
  }
(Note for people who don't know C++: this is a file-static function, which is basically as private as it gets in C++, and it's also a pure function, not by any language feature though. It could access globals.)

It has a couple of sister functions like isWaitingForWhatever() or isLocalLogin().

The naive alternative is a very nasty and error-prone forest of booleans, each of which you must remember to update when something about the connection changes, and to make sure it's all consistent. It's almost impossible to get right without exhaustive testing.


All well and good, but where do you put the damn state?


Somehow I often end up with classes containing lists or hash tables containing structs, more often than other people apparently. A technique that is IMO underused is getting creative with the key in a hash table or an ordered map - it does not have to be a primitive type, and even an integer can be divided into ranges or an integer plus a few bit-flags.

I also like to use enums, but these are widely used anyway.

It's hard to say something general because the answer is "it depends".


> OOP as frequently implemented

This is the real issue with OOP. Abusing a type system, using inheritance when composition or interfaces would be more appropriate, etc.

I've seen pretty much every programming silver bullet implemented in the most horrifying ways by people who didn't understand the reasoning behind each approach.

You can write great FORTH and terrible LISP. You can write readable FORTRAN or APL (I'm stretching it a bit here) and elegant 6502 assembly. You can even write resilient and reusable JavaScript and PHP if you have the discipline to do it.

> If you're writing a service doing requests, you want as minimal state as possible.

The service can have a lot of state. What you really don't want is your client trying to keep track of it. When tempted to do so, you need to change the service.


I like the Elmish way of explicitly managing state. You have one model that changes. Any model state can be rendered. The state is obvious and clear. If you want to test something just creat that state and test it. No need to click 10 buttons just to get the UI into the state where your bug was found.


Elegant Objects by Yegor Bugayenko actually argues against mutation in OOP for the same reasons that FP advocates due (bad for concurrency, hard to test, hidden state is hard to keep in your head, et cetera), but then all you get are namespaces with functions that act on a (usually) single data type (i.e. the class itself and its properties).

OOP itself could be good for problems where you need state machines. The Erlang Actor model is successful for a reason, but I wouldn't apply OTP to general programming.


Nothing that the typical junior engineer does with OOP can't be done with functions and well organized files. If they can't be trusted to do that well they shouldn't be writing OOP code.


When everything's a function, junior programmers treat the entire set of available functions as reasonable things to call at any time. With OO, they at least have to think about how to organize things by which interfaces are available.

People love to hate on Java, but a Spring app with everything set up into a graph of interacting interfaces is about the most well-organized code you'll ever see, and it encourages modifications that maintain thoughtful organization.


Your view isn't simplistic, I think a vast majority of developers would agree with it. State, however, is necessary for the purpose of creating useful and in some cases performant programs. Furthermore, in some cases state can make a program quite a bit easier to write and even read.

I think there are much more interesting things to be said about state than just to minimize it. For example, you can limit stateful computations inside a function in such a way that the function itself is still referentially transparent (it behaves as if it wouldn't have state). In this way, you can still do a for-loop, or do a quick-sort on a copy of the input data, without losing the benefits of pure functions. In the D programming language, this can be expressed in the type system.

We need to 'deal with' all the risks that state involve, reducing it is the first thing to do but then there are a lot of other options as well.


Wow, now we just need to stop interacting with anything stateful, like the real world!


> This is why simulations like say someone making a game or simcity with like relatively independent entities that map to something in real life use OOP.

I don't think this is the case, or at least it hasn't been for quite awhile.

Any gamedev I've known in the last decade or so would reach for an ECS[0] if they wanted to clone SimCity, in other words they would design it somewhat like a normalized database.

Each character, building, zone, or whatever in the game would be an Entity, represented by a unique ID.

Then there would be collections of Components, which are basic structs like Position or Sprite. A set of components that are all tied to the same ID would represent a single Entity the same as if it were an instance of an Entity class. These components together hold all of the game data, to the point where a naive save game system could just serialize all the (non-pointer) component data and be done. How these are stored varies by implementation and configuration, but a table in a database is a reasonable mental model to understand the core concept.

The game logic is executed in Systems, which are functions that read and write component data on some schedule. The simplest example is a velocity system, that would find all the entities with both a Position component and a Velocity component and update the Position accordingly.

In the case of a SimCity style game, the ECS approach is much more cache friendly, for both instructions and data, because you're handling all of the same work at the same time, instead of updating each entity one at a time which leads to cache miss after cache miss. This can bump the max number of agents in your simulation by multiple orders of magnitude.

Some other benefits are:

* Empower designers to iterate more quickly by giving them an editor where they can change components out without changing code. Say you have Players and Enemies and they both have Health, and Walls which are simply props with Collision. If you want to try destructible environments you can simply add a Health component to your Walls in the editor rather than try to move Walls into your LivingEntity inheritance chain or modify everywhere that does damage to check for WallEntity in addition to LivingEntity.

* Easier to parallelize. If you're using objects and you want to start multithreading you quickly start feeling like mutexes are the only answer. But with an ECS if your Systems only operate on their arguments, then you can run any systems in parallel that you want, as long as anything you mutate is not referenced in any other currently running systems. For instance every Rust-based ECS I've ever seen does this out of the box, because they can tell what fields are mutable from the function signature.

* Easier to test. If all your movement system cares about are entities with both Position and Velocity, then that's all you need to setup to perform a test. No MockPlayerInput or headless rendering required, except where those are actually the thing under test.

[0]: https://en.wikipedia.org/wiki/Entity_component_system


I appreciate that info! Its an abstraction that I had used in the past to demonstrate to other engineers that I have thought seemed somewhat useful about OOP, but as I was typing I thought to myself I'd bet that modern performant games wouldn't use active mutable entities but rather abstract into systems that change state at certain ticks so state can be much more easily managed, reasoned about and optimized.

This sort of begs the question: where does classical OOP, the one taught to all undergrad CS majors in programs that use Java or C++, really fit in nowadays?


> This sort of begs the question: where does classical OOP, the one taught to all undergrad CS majors in programs that use Java or C++, really fit in nowadays?

I have no idea. The smalltalk ideal of OOP lives on in all sorts of ways, but the deep inheritance chain version that gets taught in classes? I don't think it has a place apart from maintaining existing code.


I like to call that style "modeling a taxonomy of the world". Even real-world carefully studied taxonomies change all the time, good luck adapting your code base when an employee is also a customer. It was a crap style from the very beginning.

Smalltalk style OOP has its own issues, but there's lots of good aspects to it and you really have to experience Smalltalk as a complete system to really get it, it's nothing like Simula/C++/Java-style OOP.

That said, best thing you can learn in your programming career is to get rid of "Customer" classes (or whatever the equivalent is for your particular problem). A customer is a unique entity represented by an ID, a private key if you will, which links data in various systems.

Need to split some part of your codebase that handles authentication into a separate microservice for whatever reason? No problem, the same key can be used to refer to data in that system.

Your program magically becomes more modular, more maintainable, easier to understand, and more performant, all in one go.

Use OOP when you need an abstract interface to something that can be implemented in various different ways, e.g. data structures or plugin systems, stuff like that. Any time I see "PODs" full of getters and setters I cry on the inside (and sometimes on the outside).


Would you mind explaining this part a little more - I do not get the difference between a Customer class and a customer as a unique entity represented by an ID?

"That said, best thing you can learn in your programming career is to get rid of "Customer" classes (or whatever the equivalent is for your particular problem). A customer is a unique entity represented by an ID, a private key if you will, which links data in various systems."


Hi q-base, it all comes down to the single responsibility principle. What is the single responsibility of the Customer class?

What methods should it have? What does a Customer do exactly? Or is it just a POD which simply stores customer data? If so which data? Does it mix authentication information with the user's purchase history? Maybe not that but what about the user's little Avatar in the UI?

The answer is none of the above, the single responsibility of the Customer class is to identify a user, that's it. In the end that's just a number, no need for a class. The purchase history of a user is only relevant to the system that manages the purchase history. The avatar is only relevant to the UI. The authentication information to the authentication service.

A Customer is not in and of itself an "entity" of some sort with associated behavior in the OO sense. But you see this all the time with companies trying to model these taxonomies of their business as class hierarchies.

It's an unhelpful practice that imposes a structure on your code that's not relevant in a any way to the actual functionality of your software.


Thank you very much for for detailed response. Now I understand where you are going with it. It seems very powerful and would illicit endless expandability. Because I have not thought about modelling it on such abstract level before I am left with some questions as to how complex a model this leaves. I do not know if I am too locked in OOP-land but I like that with this thinking, you can have a SSN table for instance that references this ID and limit access to it on that level instead of having to scramble parts of tables for instance. I can also see it being useful for endless additions of tables that can relate to that ID.

But I cannot fully comprehend in which scenarios this would be a clear winner and which it would add too much overhead. There is something to it though, so thanks for opening my eyes.


What is a POD?


"Plain Old Data". An OO term for a simple record/struct with getters and setters and no associated behavior.


GUIs actually map quite well to that OOP model. Different sort of widgets where only the behavior is changed In children.


So does any hardware.


The deepest inheritance chains I saw in practice were the OOP GUI programming toolkits, they'd go six-seven deep and invariable started needing to clone logic to avoid diamond issues (they were always single inheritance it seemed).

Honestly OOP did a lot better in those than the procedural toolkits they replaced in many ways. It was generally "better".

So if we're going to dump OOP for ??? I would say that a really amazing GUI toolkit that is pretty clearly better than the classical OOP inheritance model would really underscore the point. I can see a compositional interface GUI toolkit that is great.

I have since those days not done a native UI, so all of the modern ones, from KDE/Qt, GNOME, whatever the hell replaced MFC on windows, etc I am clueless about.

Plus all UIs are kind of dominated by HTML/CSS/javascript style of code, which is its own special evolutionary tree at this point.


> The smalltalk ideal of OOP lives on in all sorts of ways, but the deep inheritance chain version that gets taught in classes?

What is the deepest chain we find in SmallTalk itself? I can't check it right now, but I'm sure it's not as deep as some classroom examples I've seen. Inheritance makes a lot of sense when you are modelling the world, but most structures we see in the real world aren't that deep.


Just looked in the current Squeak trunk image as an example. The max depth of a Class from Object (rather than from the metaclasses like Behavior, ProtoObject, etc -- these to me are irrelevant to the count) is 8. There are very few 8, 7, or even 6 deep classes. The vast majority are between the 1-3 range.


> This sort of begs the question: where does classical OOP, the one taught to all undergrad CS majors in programs that use Java or C++, really fit in nowadays?

Hot take: nowhere. I mostly kid. But really I don't know of any domain in which strict inheritance and data hiding beats mixins/composition, interfaces, and dataclasses. It's so much easier to reason about the behavior of an interface in a given role, than an "Object" which spans all kinds of scopes.


> This sort of begs the question: where does classical OOP, the one taught to all undergrad CS majors in programs that use Java or C++, really fit in nowadays?

It is not clear that such strawman OOP was ever really taught, let alone ever practiced. Mainly it seems to exist as something to be disparaged.


I have worked on several very large codebases for companies you've heard of in which mutable state, OOP and inheritance are used very heavily over the years. Its not a strawman - it has, despite everything I find wrong with it, generated working systems. Granted I've found them very bug prone and difficult to change compared to others, but there's very large sets of code and coders out there who use it all the time. Its also literally taught as if its a fundamental building block and thats what people will be using all the time.


Oh you sweet summer child :). Have an upvote just because I'm happy someone managed to avoid it. Did you know Facebook's iOS app has around 18000 classes? Yeah.

Edit: It's actually the iOS app, not the Android app, crazy either way. But yeah in university I was taught the whole "a cat is a feline which is a mammal which is an animal" shtick. Completely useless in the real world.


There is nothing wrong with ontological reasoning. A cat is a feline, how you express that relationship, or if it is even worth expressing, is another matter. We also learned “Cat(x) :- Feline(x)”, but never used that either (and no one ever derides the use of predicate logic in programming). I think we spent most of our smalltalk time covering metaobjects (I did my CS program before Java took over).


The problem with the OO style taxonomy is that doesn't just model the taxonomy but it also structures your code, that's usually where the problem is, and it leads to issues like what is the proper superclass: Square or Rectangle.

A square is a kind of rectangle but the "API" of a Square is more restrictive than that of a Rectangle (where both width and height can change independently). I've seen this issue discussed in either this year's or last year's CPP-CON...


> The problem with the OO style taxonomy is that doesn't just model the taxonomy but it also structures your code, that's usually where the problem is, and it leads to issues like what is the proper superclass: Square or Rectangle.

“Rectangle" is a superclass of “Square”, if you are referring to the entities in geometry.

> A square is a kind of rectangle but the "API" of a Square is more restrictive than that of a Rectangle (where both width and height can change independently)

Neither the width nor the height of either can change without changing the identity, just as neither the whole or fractional part of a real number can change without changing what number it is.

If you've got something with mutable side lengths, it's no longer a Square or Rectangle, it's probably some kind of Drawable that might have a mutable state variable for position and a mutable state variable for a shape, but just as numbers, but geometric shapes themselves, like numbers, are immutable values (they can be in a mutable container, but then you are changing which one is in the container, not changing the shape while retaining it's identity.)


The Square vs Rectangle issue is just an example. They shouldn't even be modeled as a hierarchy they should just be simple structs. It's just an example on how OO taxonomies are a silly (and detrimental) exercise.


> The Square vs Rectangle issue is just an example.

Yes, it's an example of a problem that has nothing to do with OOP and everything to do with applying model concepts from one domain to a different domain.

> They shouldn't even be modeled as a hierarchy they should just be simple structs.

Whether they are structs or objects with state and attached methods is an orthogonal concern to whether they form a type heirarchy. It's true that geometric squares and rectangles make sense as value types like structs. They also form part of a natural heirarchy that it is perfectly useful to leverage in code.

> It's just an example on how OO taxonomies are a silly (and detrimental) exercise.

Except that isn't what it is an example of. It's an example of why you can't apply a heirarchy from geometry to things which don't represent the concepts that heirarchy applies to. It doesn't show that OO taxonomies are silly or detrimental.


We will just have to agree to disagree then, for me the Square vs Rectangle inheritance issue is the simplest possible example of the problem, where trying to model an inheritance hierarchy "the right way" directly impacts the structure of your code in a detrimental way, and the solution is to avoid OOP silliness in its entirety.

Even in the immutable case if you have a Square inherit from Rectangle it still stores 2 separate fields for width and height (that it inherited), which are unnecessary. The only winning move is not to play.


You are building a straw man from your misunderstanding of the OOP technique. As 'dragonwriter' tried to explain it to you that mathematical model of a square being a kind of a rectangle has nothing to do with OOP modeling. Liskov substitution principle tells you that a rectangle cannot be a super class of a square.


If modeling taxonomies of the world is not OOP modeling, what is exactly? The very reason Barbara Liskov had to point out the damn principle is that people were doing dumb stuff like this.

They still do btw. Universities still teach that Mammals have a walk() method that you implement for Dog and Cat, except then you need to add a Whale and now you're screwed. Silly example, but real world cases are much more subtle. I have seen plenty of NotImplementedExceptions strung across various codebases.

If you haven't done Domain-Driven Design, with cute UML diagrams and everything, then we aren't talking about the same thing (and I have no idea what you are talking about).

Reflecting the business domain into class hierarchies and structuring your codebase around that structure has ruined more codebases than NULL ever did. Code structure should not be dictated by business taxonomy concerns, only by concrete business data.


> If modeling taxonomies of the world is not OOP modeling, what is exactly?

It is, but modelling the taxonomy of a different domain than the one you are operating in is not. “An mutable object whose current state will always correspond to some square and can be mutated to correspond to any square” and “A mutable object whose current state will always correspond to some rectangle and can be mutated to represent any rectangle” (which are the entities in the domain being discussed) are not the same things as “a square” and “a rectangle” (entities in the domain of geometry), and taking the is-a relationship that holds between the latter and trying to apply it to the former is bad modelling at a level prior to how it reduces to implementation in any particular programming paradigm.

> Universities still teach that Mammals have a walk() method that you implement for Dog and Cat, except then you need to add a Whale and now you're screwed.

To the extent this is true, it's a pedagogical problem not an paradigmatic one.

> If you haven't done Domain-Driven Design, with cute UML diagrams and everything

DDD is at least 3 decades more recent than OOP, and is not equivalent to it.


> It is, but modelling the taxonomy of a different domain than the one you are operating in is not.

What I'm trying to get across is that this happens all the time. All the freaking time. I will concede that attributing this to a flaw in OOP might be unfair to OOP, but this comment thread started from a comment on how this style of OOP modelling is a strawman. It's not a strawman because I've seen this happen all the time. Universities still teach it really poorly. It is still discussed in conferences.

Now, you can do proper OOP modelling that is actually useful. My approach is to use inheritance exclusively for the purpose of code reuse and even then only when it is trivially correct (if you need to think about it, that's enough of a sign to not use it). Interfaces are for is-a relationships so you can have a DAG instead of a tree (which is too limiting in practice). The other 3 pillars of OOP are very useful, meaning abstraction, subtype polymorphism and encapsulation.

But I only use OOP to model software entities that have actual behavior + data, not business concerns (I will rant about how "Customer" classes with associated methods are a bad idea till the cows come home, they break the single responsibility principle by default).

But, and this may be more of DDD problem than a OOP problem, I've seen people invest tons of effort into modeling UML diagrams where every class corresponds to some business concept, and then they think of all the little methods these classes should have, and then this gets turned into code.

The design is *always* wrong because the behavior is associated to the wrong data, and then you end up with the single responsibility principle broken, horrifically, everytime. The performance is abysmal because state is spread across RAM like my cats spread sand all over the house.

Maintenance is also a pain because you feel like you need to maintain this utterly suboptimal class hierarchy to fit the "business taxonomy" even if it doesn't help in any way with transforming data A into data B, so you add all these additional classes and abstractions and dependency injection to make it somewhat usable in practice.

Things get a lot easier when you focus on using paradigms as tools rather than ideals.


>If modeling taxonomies of the world is not OOP modeling, what is exactly?

You are still focused on a mathematical property of a square being a special case of a rectangle. Inheritance is not the tool to model such a relationship. Again it has nothing to do with taxonomy.

>Universities still teach,...

The same people teach functional programming with silly recursion examples and linked lists being the most important data structures.

OOP code can be convoluted, but a large imperative mess is worse. I haven't seen any large collaborative project written in a purely functional language, so I can't comment about that.


I'm not suggesting getting rid of OOP or switching to functional code. I've seen far worse horrors on "functional" codebases. Rather I'm merely complaining of something I've seen happen in practice, from companies that are "proudly OOP". The moment you turn a tool into an ideal, you end up with a mess. Doesn't matter what it is.

You're too hung up on the square vs rectangle example. It's just an example. Replace it with a big tree-shaped UML model encoding business concepts, and it's the same problem. Business concept relationships are a DAG or even an undirected graph and no edge in that graph should be given "preferential treatment" (as is the case for inheritance relationships, since they form a tree). The moment you do, you screwed up, and people do all the time.


> Liskov substitution principle tells you that a rectangle cannot be a super class of a square.

It does not. The Liskov substitution principle states, as it applies here, that a property that holds for all rectangles has to hold for all squares. This is true, as every square is a rectangle.


A constant rectangle can be a superclass of a constant square; a mutable rectangle cannot be a superclass of a mutable square (without some kind of type changing mutation?).

I think this generalizes to any "this is a more constrained that".


If the square and rectangles are not mutable, you can indeed treat a square as a rectangle (however you want to express that type). If they are mutable, then a square is of course not a rectangle.

In OOP systems that support predicate-style dynamic inheritance, rectangles pick up the square trait only when their width is equal to their height. We don't talk about such systems very much today (JavaScript does not have a notion of type, and updating an object's type is a mutable operation rather than one driven by a rule), but there were experimental systems from the 90s that looked at this.


Pretty cool to hear about those, didn't know there was such a dynamic approach to the problem.

Either way, the problem in my view is that the question is silly to begin with, Squares and Rectangles should be PODs (or, even better, simple structures), and not have any associated behavior. The decision of whether they are mutable or not then depends solely on the needs of the software, and not on irrelevant modelling constraints unrelated to the problem.

There's no need for them to have a taxonomy relation unless the problem being solved involves taxonomies of shapes.


What have you solved by having squares and rectangles as PODs? Now you won't be able to treat them as same in some way (e.g. IShape, IDrawable, IPrintable, IArea,...) and you will need twice as many functions for doing same things.


If you make a square and a rectangle into IShapes (with no inheritance hierarchy) you still need 2 separate implementations for GetWidth().

Either way, it's simply not true that you can't treat them the same way because you've made them PODs, there are more kinds of polymorphism beyond subtype polymorphism. Even for the latter modern languages split the data (structs) from the behavior (traits/protocols), see Rust or Swift for example.

If you explicitly need to treat squares and rectangles as a single entity to draw them (alongside other shapes), rather than thinking of each of them having a "draw()" function (likely breaking the single responsibility principle), you should instead have a function square_to_drawable, rectangle_to_drawable etc, where a drawable is also a POD but one that has the relevant drawing data (textures, triangles, whatever). Then a drawing system is responsible for actually rendering these drawables.

That's how it's done on modern game engines. It takes a while to wrap your head around but it works really well. This style is used a lot by modern game engines because performance is way better with this style.


Everything you are describing is still OOP. Again, inheritance in not necessarily the best tool to model all relationships.


The most basic principle of OOP is the bundling of behavior and data. If you don't have that, you don't have OOP.

PODs + independent functions is not OOP. ECS-style designs are not OOP because ECS-style designs are PODs + independent functions (it's a relational model). OOP and the relational model are not equivalent, if they were the object-relational impedance mismatch wouldn't exist.

Polymorphism exists independently of OOP. OOP is not the only way to model relationships. OOP is not the only way to encapsulate data. OOP is not the only way to abstract things. Sometimes it is by far the best way to do all three. That's when you use it, not because of some silly attempt at purity of style.

People have a really distorted view of procedural-style programming where it's all global variables and copy pasted code. Couldn't be further from the truth. Even within OO codebases you have tons of procedural-style code that just happens to be stored in a class with the little "static" keyword in front. Many "procedural" codebases (i.e. stuff written in C) have lots of OO-style code with structs full of function pointers.

But it's wrong to say that it's all just OOP in disguise. If behavior and data are not bundled it's not OOP, and most code doesn't need that, not even when you need abstraction + polymorphism. Encapsulation is the main one where OOP is often the best approach.


> This sort of begs the question: where does classical OOP, the one taught to all undergrad CS majors in programs that use Java or C++, really fit in nowadays?

As everyone is telling you, it’s a poor paradigm that makes the developer’s job harder compared to both non-OOP approaches and better approaches (mixins / multiple inheritance).

That said, one should be cognizant of the reason it was created in the first place - single inheritance allows C++ to implement fast polymorphism via virtual function tables.


> single inheritance allows C++ to implement fast polymorphism via virtual function

Multiple inheritance can be implemented to be as fast as single inheritance regarding virtual function calls, and typically is in C++ by keeping multiple virtual table pointers per object. The problem this brings is that we can have pointers/references to the different parts of the same same object. A Child* may point to the start of the object, but Parent* may point to somewhere in the middle (because that's where the parent's vtable pointer happens to be). This also means that casting a pointer can change it. This can introduce some "interesting" bugs, as you can imagine.

OTOH, there are languages like C# that don't do that, but require 2 "hops" when calling an interface method, causing a slight performance penalty (a class can inherit from at most one other class, but can implement multiple interfaces). But a reference to an object always points to the object's start, which is very important for garbage collector.


C++ supports multiple inheritance though. And it still does it with (multiple) vtables. Did you mean single dispatch?


No, I just had an incorrect memory / mental model of how C++ vtables work.


Yes, but multiple dispatch is much slower than single dispatch.


Even if you not a game-dev. I urge you to look into ECS just as a mental-exercise. There are some excellent YouTube talks on the subjects.

Many of us are 'stucked' wring the same old CRUD-Variation-Apps stuff. Learning about ECS is a great way to get those "original-programming-excitement-juices" flowing :)

The gaming industry is renowned for some fantastic programming solutions. To ekt out every bit of performance.

Anywhoo - makes for a nice change to arguing with colleagues over ORMs, Fat-Vs-Thing models :P


I doubt any professional gamedev would reach for ECS. No major game engine has a finished ECS system yet. You would have to roll it all yourself and shoehorn it into the engine somehow.

Unity has not shipped DOTS. They actually removed it from the Package Manager last year. Joachim says it has a bright future, but I suspect it will never ship in Unity itself.

Epic has nothing in Unreal yet, though apparently a few months ago somebody spotted some changes in repository that suggests one may be on the way.


> No major game engine has a finished ECS system yet.

The major engines may not have ECS built in, but some of them are supported by ECS systems that are readily available. Your implicit restriction of professional gamedevs to include only people who both use a major game engine but don't use third party components not supplied by the engine is, I think, overly restrictive.


I expect the programmer who liked working with an ECS system would have a hard time making a business case for taking on the additional risk of a third party ECS foundation.

A traditional OO architecture will provide all the performance you need to create a modern Simcity style game without having to take on the additional problems of potential bugs in the underlying code, training everybody to understand and use it, and not even really knowing if the end result will be significantly faster or not.

Where I work we think long and hard about adding any third-party packages to the project. The benefits have to be very real, for the budget or for the player.


There are a tonne of open source, MIT licensed ECS libraries in pretty much every language you can write a game in. And, at the end of the day, ECS frameworks aren't all that complicated at their core, it's not hard to roll your own, though libraries will have better querying features and optimisations.

The benefit is to the developer, arguably the most important part of the game development process. It can save a lot of headaches that OO hierarchies can create, makes things more easily concurrent, allows for more flexibility in behaviour and emergent gameplay.

ECS helps to prevent bugs in game object logic, by keeping state and behaviour separate from its corresponding entity. If I'm looking for some behavior, I don't have to start thinking about which ring of the inheritance chain it ended up on, I just find the component or system that does that thing. It encourages you to write state and behaviour in a way that works with any entity and can be attached to anything at any time without crashes and state mutation problems or race conditions.


Dungeon Siege[0] shipped in 2002. Unity began their ECS journey by sniping talent from Insomniac. ECS is not an unproven concept in someone's head.

That Unity and Unreal are lacking (even after Unity's public efforts in the area) is because they are licensed en masse and retrofitting each with an ECS would be no small feat. And is that refactor worth losing revenue from obsoleted marketplace content? Or does the ECS need to interoperate perfectly with existing code from endless numbers of existing projects while still providing the benefits of a dedicated solution?

Unity and Unreal are not the only engines. In house engines are shaped by the needs of the immediate users, and not the sales pitch for hobbyists or third party studios.

[0]: https://www.gamedevs.org/uploads/data-driven-game-object-sys...


Does Freeciv use this approach? Or any other significant open source game or roguelike?

ECS sounds somewhat like how I was thinking of a civ-like game engine while falling asleep a couple months ago when I was thinking why Civ scaled so poorly under some circumstances: traditional games used to have everything in memory, but civ type stuff could just have a database with some indexes and spatial indexes for quick lookups, but otherwise just sweep the various tables. Thus the size of your game was more constrained by disk space than RAM.

Per your discussions with cache conservation/thrash avoidance, does ECS work well for mapping entities to specific processors so that cache-hopping doesn't occur in modern multicore processors and NUMA stuff?


what about when the abstractions age and a new feature for a system needs the data from the components from another system?

the long term answer is probably a refactor, but what's the common quick fix? copying/duplicating data between component types? systems that examine other components as well as their native ones? merging systems?

asking the hard question... how does it stand up to the unpleasant cases?


> asking the hard question... how does it stand up to the unpleasant cases?

It's the best tool I know of for a certain class of problem, that's all I'm claiming. Not evangelizing it as a magical cure-all for all domains, just explaining it in enough detail that it can be understood by someone outside the domain of gaming.

> what about when the abstractions age and a new feature for a system needs the data from the components from another system?

> the long term answer is probably a refactor, but what's the common quick fix? copying/duplicating data between component types? systems that examine other components as well as their native ones? merging systems?

I apologize if I gave the impression that things are so tightly coupled that a system owns a component or vice versa. (Though if I had to pick one, it'd be that components have associated systems)

In reality your components are just plain structs, and any systems that want access can query for entities based on any combination of components. (and good ECS impls allow for exclusion as well)

For instance I mentioned a Position component. This would be used by a movement system that checks for (Position, Velocity). It would also be used by a rendering system, which could query for (Position, Mesh) or (Position, Sprite) as appropriate. It could be used for collision by querying for (Position, Bounds).

If later you want to unload entities that are too far away from a player, you could perform two queries: (Player, Position) and (Position), filter out entities in the second query that are within n units of an entity in the first query, and then despawn what remains.

No existing data or systems need to change to allow multiple systems access to the same data. The only thing that might change is if the engine provides automatic parallelization, and you have two systems that mutate the same data, you may need to define an explicit ordering for them and they would not run simultaneously. If you don't need explicit ordering you may not even need a code change in this case.

***

In the spirit of what you're asking though, let's say you've been making a tile-based game where the player can move between discrete spaces like in chess, but you decide you'd rather have free movement like Mario. Your Position component uses integers instead of floating point values and you don't want to change your world's scaling. Here you have a few options:

0. You could just bite the bullet and change the types on the Position component to floats instead of ints, and let your type system guide you to any errors. Then you run your test suite again and make sure that everything is still behaving as expected. I'd also plan on creating several new tests based on analyzing existing uses of Position. And of course play your game again to make sure it feels right.

1. You can store the fractional position in a separate component, PositionFraction, and create/update systems as needed. Movement would need to be updated to look for (Position, PositionFraction, Velocity) and rendering would need to be updated to look for (Position, PositionFraction, Sprite). Meanwhile pathfinding could still just look for (Position, Goal).

2. You can create a second component that holds the full float value called PositionFine. Like above you update the systems that care about fine-grained positioning to use PositionFine instead of Position. Then you create a system to update Position based on PositionFine's value or vice versa, and log anytime there's a discrepancy. Once you're confident that you can drop Position, you replace every use with PositionFine. Rename afterward as desired.

If I'm changing the meaning of an existing component like this, #0 would be my strong choice, but if the game is already in production and the migration needs to go over super smoothly I'd consider #2 as well. In particular you can treat the existing logic as the source of truth, but run the new logic side-by-side and log out any variation between the two for analysis before flipping the switch and preferring the new logic.

However if the new data is purely additional and makes sense being split off from the existing data, then #1 is the solution to use. Think migrating from displaying usernames over players' heads to letting them choose a custom name. You still need the username for authentication, save games, friends lists, and the like. But a new component for the display name lets you reference that when it makes sense.


thanks for humoring me! i'm writing because i'm genuinely interested.

so it seems like then, that the discipline in building a system in this fashion revolves around responsibility for updates (which system is the sole updater of a given type of component) and the sequencing of those systems (ensuring that all the systems that update components that are used by a given system have completed their updates, possibly with some sort of record level update indication that allows for downstream systems to begin processing before upstream systems have completed all their records).

do any of the ecs libraries provide facilities for these problems, or are they typically built into a game engine framework?


That’s a very good question, because sequencing tends to be the place where implementations vary the most.

Usually there will be some way to sort systems into steps and define the order of those steps. Some require you to hardcode the order, others define constraints like “after input” or “before physics” and attempt to solve those constraints, and some let you define groups that can run together and you define the order of the groups. Explicit signaling is not typically built in, but you may be able to implement it yourself if desired.

In environments with an expressive type system, most tend to favor the constraints model. Otherwise ordering tends toward the hardcoded approach, typically implicitly in registration order.

As to managing what systems are responsible for updating a given component’s state, that tends to be left to the game developer.

Sometimes there is a concept of events and if it’s not obvious who should mutate something then systems that want to mutate will send events that later systems can consume. For instance an input system and an AI system might both send a Move(entity, direction) event that a later system validates and applies.

And because it’s cute to do so, often times those events will be implemented as components themselves, with convenience wrappers to make it feel more natural to end user developers. This can come in handy for networked games and debugging in editor. You could also use it in game, such as displaying a unit’s next planned move in a turn-based game.


interesting. this is really cool!

in the automatic constraint solver variants (which is more interesting to me) are the schedules static (precomputed at compile time) or do they run as a dynamic scheduler of sorts and are the constraints statically checked for deadlock ahead of time?

this architecture is exciting!


Compile time would be pretty sweet. I've never looked into it so I don't know for sure.

Don't get too caught up in the hype, it never ends well no matter what the architecture. But it's certainly useful and fun to play with.


Hot take: OO is more powerful when you embrace stateful objects. As long as you are dealing with stateless objects, many other techniques have plenty of advantages.

But, consider, OO grew in a time when the likes of Logo was strong. How do you draw a square in Logo? Usually, some form of:

    pen down
    repeat 4
        straight 10
        right 90 degrees
But this /only/ works if you keep track of the state of the system in your mind, when your are figuring it out. Which, works really well if you are being taught that your program doesn't exist in and of itself, but to manipulate something else.

Functional advocates lose many learners because they don't allow that writing a local function can be done with the more global state in mind.


But then you're mixing up the state of the system with the shape you want to draw. If you were now working with 2 pens, you'd have to rewrite your shape from scratch too, not just your rendering, to speed up the output.

Better to separate the shape data, which is immutable (and basically declarative), and the rendering method, which does need to know about the previous work which was already completed and what it is doing right now.


Maybe. There is a reason gcode exists. Sometimes you really are controlling a single pen.


Gcode exists because hardware abstractions are even more leaky than software extractions. When pixels go onto your screen, you don't assume to know better than the guy who wrote the driver for the graphics card how they should get there. When plastic gets deposited on a 3D printer the way in which it is deposited actually affects the properties of the resulting object. Same for a CNC lathe or milling machine, although to a lesser degree.

There are of course also historical reasons, when it would be a central mainframe that would generate the gcode, and then it could be executed many times by cheaper computers attached to the machines. There was even a point where a lot of gcode was written, or at least edited, by hand. In these modern days of compute excess, gcode probably wouldn't have developed to the extent it did, and we'd be distributing STLs with some metadata around tolerances, materials and primary stress directions and the machines would figure it out themselves. The equivalent of gcode would just be used as a communication protocol between the interface and the motor controllers.


A polygon is a set of lines

A square is a polygon with four lines of the same length, forming 4 equal angles of 90°

A drawing can be made given a pen and a shape

The output is a drawing of square made with pen


And a line is a set of points. Your point?

That is, yes, to an extent you can expand your vocabulary to include more shapes and find ways to compose them. Or you could peel back and literally write point values. Both have uses. And both are used.

Consider, we aren't forcing all art assets to be created and described using elementary shapes. For many of those, we are closer to literally writing the canvas by hand. And then retroactively glueing to program.

It is a shame we often force a single paradigm with our code bases.


I personally think "encapsulation" as us used in OOP, is a misnomer. State is usually not encapsulated, it is just hidden. Proper state encapsulation would be to use mutable state internally for efficiency, but for that state to be unobservable externally.

OOP does unfortunately encourage introducing mutable state into the domain model. The canonical example being the back account, with a mutable back balance!

The good parts of OOP are interfaces and first-class modules. Obviously we should try and keep those.


> Proper state encapsulation would be to use mutable state internally for efficiency, but for that state to be unobservable externally.

This is literally how private/public keywords work, so I think your criticism is unfounded. However, I do agree with the overall sentiment that OOP implementations tend to "leak" way too much state than they need to.


I think you misunderstood my point. Private may protect direct access to mutable state, but the object may still have mutable state that is observable externally and must be reasoned about. In which case, the mutable state is not truly encapsulated.


That is not hidden by immutable data either - the state is just global in the latter case, in a way.

There is no getting away from essential state, from a theoretical point of view. In my opinion the often leaky partial encapsulation of state is still one of the better ways to deal with it. And immutability is another axis so of course the frequent case where it makes sense should be adhered to, but not religiously.


It is much easier to reason about immutable state than mutable state, both for humans and compilers, especially in a distributed setting (any modern piece of hardware).

The problem with "leaky" encapsulation as you put it, is the combinational explosion of the state space as many stateful objects are composed.

Most mainatream programming languages are still unfortunately not well geared up for working with immutable data, they lack even persistent collections. Functional languages are of course ideal for this.


> The problem with "leaky" encapsulation as you put it, is the combinational explosion of the state space as many stateful objects are composed.

I don’t think it has to be necessarily more than with an immutable approach. Like, there is an essential amount of state you will have to have either case and it is not clear to me that immutability is always the better choice.

But don’t get me wrong, I also default to immutable data structures, I’m just saying that 1) OOP is not incompatible with FP 2) not every problem is solved better with FP-idioms. It’s not accidental that haskell has state monads as well.


Ah, I see what you mean, though I do think that immutability purists like yourself have their own monsters to contend with (even apart from the obvious performance hit).


Absolutely we do! For example, garbage collectors are complex beasts that are hard to tune and tough to scale. But of course some OOP languages have started using GC anyway, in which case, I honestly believe they might as well be functional programmers :)


if you're writing something like a wrapper service around a database, there should be no state at all. (i'd argue it's high time that databases moved forward with respect to security and hardening such that they can be accessed nearly directly or... directly)

if you're building a thing where state is required, then yes, it should be minimized. but i'd argue against dogmatic use of immutable data records and really think about what that sort of design is attempting to achieve: reduction of sprawl in places in the code where a piece of data is updated.

the goal is to put all that stuff in one easy to find place. that can be manifested or violated (sometimes with great acrobatics) regardless of whatever rules are followed. (even oop!)


> it's high time that databases moved forward with respect to security and hardening such that they can be accessed nearly directly or... directly

Check out Hasura, Postgraphile and Postgrest.


> I worry a little that my view is overly simplistic, or maybe applicable only to domains that I have worked in. If anyone wouldn't mind poking holes in this argument or offering examples I would appreciate it.

I think this line alone is proof that you're doing this right.


I used to think that the solution to mutable state was to prohibit it and code with immutable structures all the time, but after a few years of Rust, I think it's the wrong approach.

The right way to handle mutable state is not to pretend it doesn't exist but to accept it as a reality of complex systems and to encode its management in the type system of the language. And that's exactly what Rust does.

With Rust, I no longer feel dirty whenever I have mutable state and I trust Rust to not just keep my code bug free but also to make me think carefully about mutable state and how to design my code with it in mind.


Rust prohibits shared mutable state as part of the basic language (or rather its 'safe' subset), relegating it to special-cased "interior mutability" constructs. This is essentially "as immutable as you can get" in a low-level, systems programming language. (Other languages can thread state mutations explicitly as part of a generally "immutable" design, but that doesn't give you support for the expected low-level features, so instead it's part of the language in Rust.)


It's definitely that, but to be fair the problem is caused by classg-based programming, not OOP itself.

Put state on an object only if there is a hard requirement for it. The occurrence is incredibly rare, state is mostly introduced to save re-typing method arguments...


Games are actually moving away from OOP by separating out state into a data oriented system.


But aren't they doing that mostly for performance via better memory layout and thus better cache locality? ie: arrays of objects vs objects of arrays. It's a sacrifice of code architecture for performance. I feel like games are actually a good example where OOP makes sense, since there is inherently so much state and encapsulation is useful.


> I feel like games are actually a good example where OOP makes sense

Intuitively, yes.

In practice, I think once you actually use an ECS for a game you won't want to program them in the traditional OOP way.


It's every game dev's first mistake to start adding, say, weapon types to their game by first creating an Item class, then creating a Weapon subclass, Sword inherits that, Shortsword inherits that... And so on. Makes sense, right? It's exactly what OO is for.

Except games rarely have that rigid a structure, and maybe you want a sword that also has laser gun features, or an axe that's also a magic staff. That's a lot of subclassing and further dividing up your game objects into rigid hierarchies that become difficult to modify later without unintentionally breaking stuff (was it the logic in the Sword class that grants that damage bonus, or in the Weapon class?)

It's nicer to be able to say "I have a weapon entity, I'm going to attach the `SliceDamage" component, the "MagicAttack" component, and so on, and call it a magic axe. It's easier to maintain, it can change at runtime, and the behaviour code only exists in a component file that only does that behaviour, rather than nestled in some lowest common denominator of a hierarchy of classes.


Bevy is an ECS based engine, and it’s actually very nice to use. It adds a lot of ergonomics to that experience.


I think I need help understanding this.

My understanding of data oriented systems is there's a desire to put related data physical close, in memory. It doesn't remove state, it just packs it differently.

To be honest, I'm confused by most of the comments here.


One of the benefits of a data oriented ECS setup is that it's much more cache optimal due to the way data is packed, but that's not necessarily the primary reason to use it. In fact, on the data packing implementation side, there are actually differing ways in which the data can be packed, with differing performance pros and cons depending on type of data / access pattern.

Really, the primary benefit is that you get a much cleaner paradigm to work with. Here's a good basic overview of the point of an ECS system: https://iolivia.me/posts/entity-component-system-explained/

In particular I'll point to the key quote in that post that distinguishes ECS from OOP encapsulation: "The whole idea is separating behaviour from logic, so all the data goes in components and all the behaviour goes into systems."


I bet save / load is a lot easier too! :)


In my experience many game companies were never big on OOP in the first place.


Weren’t games already written with entity component systems?


I’m happy to be corrected as I’ve only really dabbled with Indie game development on and off but my general impression is that the paradigms have gone procedural -> object oriented -> ECS.

For example, Unity is designed around GameObjects but is now adding building out the DOTS framework stack that’s more of a first party ECS system.


Nobody has ever been able to convince me that these informal object systems (eg ECS) aren’t really just OOP with different extensibility mechanisms and/or a different place to stow away object state (oh, and let’s call the objects entities instead).

The only real diversion from objects in games that I’ve seen is the work in Andrew Kennedy’s thesis on FRP for games.


Yes and no.

The EC part of ECS is definitely object oriented - more so than mainstream object-oriented systems / languages, in that it favors composition over inheritance (i.e. entities are compositions of components).

But the S in ECS breaks the most fundamental tenet of object-oriented programming - encapsulation of state. And this separation between entities/components and systems is what makes data oriented programming so much cleaner and more powerful than OOP.


If the state is encapsulated somewhere else, why is it no longer OOP? I don’t think Alan Kay would say that having each object simply be a handle into a table of values meant they could no longer be called objects. Heck, if we take the FactoryFactory example as being the bad thing about OOP, and consider that this comes from the GoF design pattern book, there is already a pattern in that book that closely resembles ripping the state out of an object and putting it somewhere else. Again, it is definitely not strawman OOP, but not very many programming systems are, especially modern ones.


Encapsulation in OOP terms means the state is encapsulated with the logic that fetches / mutates it. ECS explicitly is designed to do the opposite. It’s really that simple.

The issue isn’t about how you store the objects but about whether the framework logically encapsulates object data with its associated logic.


> Encapsulation in OOP terms means the state is encapsulated with the logic that fetches / mutates it.

No it doesn't. There are plenty of examples in OO where that isn't true or no physics engine that's been build in the last two decades would work at all. The only core thing needed for OO is that objects have unique identities that then enable a notion of associated state at all. A pure system would not allow for an unbounded number of unique identities at all, you would not really be able to talk about objects at all (or entities or whatever object synonym is preferred).


> No it doesn't.

Yes, yes it does.

> There are plenty of examples in OO where that isn't true or no physics engine that's been build in the last two decades would work at all.

It would be surprising if most physics engines were built in an object-oriented way. The only one I'm familiar with, ODE, certainly isn't object-oriented.

> The only core thing needed for OO is that objects have unique identities that then enable a notion of associated state at all. A pure system would not allow for an unbounded number of unique identities at all, you would not really be able to talk about objects at all (or entities or whatever object synonym is preferred).

No, objects in OOP have not just their own unique identities but have their own behavior. Encapsulation is the most fundamental pillar of object-oriented programming. Without that, any procedural C code that makes use of structs could be called object-oriented, since structs would then be synonymous with "objects".


ECS is a type of relational model, it has nothing to do with OO.

EDIT: just to make it clearer, both Simula Style OO and Smalltalk Style OO bundle data and behavior together. The Entity owns the Components and the System.

ECSs are much more like relational databases where an entity is a primary key, components are tables and systems are queries (that also mutate the database). The entity is subservient to the systems, not the other way around.

If somehow a relational database is in any way equivalent to OO then we've reached peak clown world as far as terminology goes.


I'm no expert, but here are my 2cents.

One of my "ahhh moments" with ECS was when someone pointed out. The ECS has a very 'strict' boundary on DATA(attributes,entities,components) VS BEHAVIOR (methods,systems,functions).

The 'traditional OOP' usually 'smash/hide/design/encapsulate' these two conflicting powers into one 'Entity'

I think it's 'easier' to write a ball-of-mud in OOP for games, then it is to write ball-of-mud with ECS.

*NOTICE I said 'easier' not 'impossible'

I love the distinctions between BEHAVIOR and DATA, from an organizational point of view. The bigger the whole system is the more value I think a good programmer can get with ECS.

Imagine having to "implement" gravity for 100 different types of 'Objects'. Sure The ECS way would be to have a System(Gravity) that executes the 'same computing' on all entities with a 'MassComponents' and 'PosComponents' for example. The Entity would usually not need to know ANYTHING about Gravity. Sure the OOP way one can inherit but eventually you inherit yourself into a corner (diamond-problem).

My 2cents - YMMY

TL;DR (just squint hard enough)

ECS ~= (Data | Behavior)

OOP ~ (Data + Behavior)


I believe that we're interacting on a server that was written to these principles.


What is state? Every local variable is state. I can convert any global variable to a local variable or function argument with trivial changes. Does it mean that every local variable is the real enemy? Only empty program is perfect. But useless.

I'd say that not just state is the real enemy. It's all about life time of a particular bytes. `(x, y) => (x + y)` is a good function. Its state is discarded pretty fast. `(x, y) => (z) => (x + y * z)` probably is a bad function, its state is preserved for a long time. Global variable state is preserved during the whole program run. Database state is preserved during the whole software lifecycle or may be even more.

And that's not even about state is enemy. It's about how much time do you need to design this particular code. When you're thinking about database structure, that's a real deal. Take a lot of time, that's important and decisions will have impact for years or even decades. When you're designing pure function which does not leak any state, you don't need to think at all, just slap something and move on, you can easily replace it later if need arises.

TLDR: prefer state with short life time, be very careful with code that works with long time state.


Mutable state, you mean, yes?


I think so, though I had in mind internal (hidden/abstracted) vs explicit states. And yeah, only in a very few cases would you want mutable states - abstracting naturally transient entities like network connections, times where memory or performance constraints demand entity resuse, etc. Though if you end up in that situation with no guardrails around that state (IE: should usually be a finite state machine) you're definitely going to have to work a lot harder.


Preach it brother.


I have not read the article but I've seen other blog posts on the subject. The issue with "OOP is bad" is that OOP means different things to different people.

Abstract Data Types are sort of a subset of OOP and are massively useful, I certainly don't think it's a good idea to expose the internal implementation of a data structure most of the time. Any sort of plugin system works in an OO matter. It is a useful tool, no question.

Even things like Actors like in Erlang (which are much closer to the Alan Kay style OO) are massively useful in a distributed setting where state is naturally distributed.

Where you get into trouble is when you go the whole "lets model a taxonomy of the world" style with inheritance hierarchies or go ham on design patterns, layers upon layers of abstraction and other nonsense like that.

The saddest thing about the "object–relational impedance mismatch" is that the focus went totally to the wrong side. The relational model is a much nicer way to model relations than a graph of objects (that's the whole point after all). SQL sucks but that's a separate issue, Entity Component Systems are a form of relational modeling for example and work really well, or even better Datalog.


The problem with the "inheritance isn't part of OO" take is that every single one of the languages we call object-oriented that gained mass adoption have inheritance, including Alan Kay's own smalltalk. There have been entire books written about object-oriented design describing ways to use (or not use) inheritance in program design. So whether or not it was intended to be important by the author of the term is immaterial, it for all intents and purposes is a crucial part of object oriented languages.

Inheritance is not the only problem w/ OO either, many things are straight up awkward to express when you must couple data and code together. Many 2+-argument function w/ disparate parameter types create confusion about which class "owns" the definition of the function.

You also don't need to marry data and code to get the benefits of encapsulation. Many functional languages in the ML family have developed clever solutions to encapsulate the definition of a datatype while exposing it to module-local functions. There's no need for the datatype to carry a method around with it to support this.


"The saddest thing about the "object–relational impedance mismatch" is that the focus went totally to the wrong side. The relational model is a much nicer way to model relations than a graph of objects (that's the whole point after all)."

I found the same thing. When I was using ORMs I always found them clunky for all but the simplest tasks, where I would long for an easy way to use SQL and have it "just work" for objects, so I created this:

https://github.com/iaindooley/PluSQL

It's obviously not been maintained but I think it's a model that has legs: that is, simply the creation of SQL with some convenience methods, allow the use of completely arbitrary SQL, and then intuit the object mapping automatically without loading the entire result set into memory.


There are many such MicroORMs, C# has a few extensions for Dapper, as well as several newer projects that directly have different querybuilders and convenience methods.


Do relational models support sum types? I find them an essential feature in programming languages, nearly as important as structs or rows.


Unfortunately many don't. Standard Datalog for example doesn't have disjunction which you need to model sum types as relations.

My preference is to have sum types modeled separately from relations as just a complex type that can be related as well. This is the approach taken by Souffle, a C++ Datalog implementation which supports sum types.


For some reason I cannot edit the post above, but I misspoke on Datalog, since it has disjunctions, just not in the head of rules. You can go pretty far with disjunctions in the body which suffices for most cases of modelling "sum types".

For example:

Stakeholder(X) :- Shareholder(X); Customer(X); CEO(X).

It's still a lot better to model them separately from the relations, for the same reason that you want to model relations between points as actual relations between points not between X, Y and X, Y.


My issue with OOP is: Design Patterns: Elements of Reusable Object-Oriented Software.

I don't take issue with the authors, with their insights, or anything related to the content of the book. It's that the book exists at all: it's a book filled with solutions to imaginary problems.

When using a procedural language the first thing you do is start implementing a solution. When using OOP, you first have to solve the imaginary problems created by OOP, only then can you start down the path of typing out a solution.

It's significantly more difficult to refactor OOP (even with great tools) than it is procedural.

If you could spend 20% less brain power, possibly 40% fewer keystrokes to describe the exact same solution, why wouldn't you?


>solutions to imaginary problems

This is a fundamental misunderstanding of what patterns are. The GOF book is used to this though.

A design pattern is someting that will naturally crop up if you adhere to certain design principles. If you follow a principle of separating instantiation logic from other logic then you will start to see factories. If you combine multiple complex parts of your code into simpler ones then you will see facades.

GOF is a reference book for some patterns that have been observed as being common, with examples that are essentially academic. It is not a how-to guide for OOP.


But many programmers use it as a how-to guide. I've had people tell me "but that's how it's done in GOF", even if the design pattern was a poor fit for the problem at hand, just because there was some superficial resemblance to one of the examples in the book.


Ask them to re read chap.1 or 2 where it says black-on-white that this is not a set of mandatory rules. (Same for Clean Code by the way which is often taken as gospel against the wishes of its own author!)


I remember trying really hard to find case where I could use a pattern in my code as a junior dev.


but this is not the fault of the book but of bad teaching.


That exact sentiment exists deep in my comment history here: I think I used the word "blueprint" if you care enough to fact check that. That is why I said I don't hold issue with the content of the book.

My issue is that the book is useful. It helps solve the artificial complexity introduced by OOP.


Agreed that it's never good to have new social problems created by whatever you did to solve the previous problem. But that's the nature of doing stuff.

I own a car. Great, now I have maintenance concerns. But it's still a net positive, which is why we do it.

If oop creates more work than the value of brings, then we should scrap it. But I've seen some pretty bad procedural code, so I don't think that's a given.


I see. It was the ambiguity over your usage of 'imaginary problems'. But yes - your previous comments seem to suggest we're actually on the same page regarding patterns.


But even factories and all these terms are very vacuous and only present because objects are giving bad solutions.


I put it to you that the OOP concept of a factory exists because in OOP your factory is often (though not always) a type, and types tend to need names.

In e.g. FP there are no objects and therefore no instantiation but you can separate this kind of logic all the same, except you wouldn't call it anything, or would name it like any other function.

For the record I've been doing mainly OOP and have dabbled in F# so I'm happy to learn something today.


That's the whole point. OO gives a class as base, forcing you to reextract limited instanciation logic into another thing, when it's just functions from a to b. And people get to waste time on this.


No - that's not what factories are at all.

If you want a simple function to create objects, you have those in OOP languages. They're called constructors. They don't even have names independent of the thing they construct, so they're as lightweight as you can get. And if you do want names, you have static methods or top level named functions (depending on language).

Factories exist for a few different reasons:

1. When there are more things to configure about the newly created objects than makes sense to pass as function properties.

2. When an API designer wishes to provide backwards compatibility to his clients, which adding function parameters doesn't do.

3. When there's a need to separate how something is constructed from where it is constructed, i.e. you create a "factory" or "builder", configure what you want it to do, but then don't directly use it. Instead you pass it off to some other code that uses it when it needs to.

These are fundamental concepts. They aren't some side effect of how OO languages work. I think the disdain for them comes largely from programmers who haven't actually done many different types of programming. If all you've ever done is write web apps then yeah they're sometimes going to seem a bit useless. If you've shipped a type safe library API that you've needed to maintain compatibility for over a period of many years, maybe that will be used in ways you didn't anticipate so you can't just arbitrary refactor yourself out of a hole, suddenly these sorts of abstractions start to look pretty good.


point 3 is exactly what I'm saying


But the concept of a factory is independent from whether you name it. In Java you can write a factory like this:

    () -> new Thing(foo, bar);
Behind the scenes it's a class, but you don't write it as such and you don't name it. Nonetheless, it's still a factory.


It's vacuous concept.

Also you're leveraging anonymous function syntax which is, interesting, to say the least. In Java 8- you'd have to write boilerplate class named after a pattern. Lastly creating something from outside information was done since forever, it's trivial when you don't invent ceremony around it. Hence my conclusion, it's a waste of time and effort.


No, because you still need to name the thing the function is assigned to. What exactly should the user of the function call it? thingMakingFunction? You need a consistent way to refer to "a function call that creates an object in a particular state" and factory is as good a word as any.

Yes, in Java 7 or below it'd have required more verbose syntax. So what? That came out 8 years ago and Java isn't the only OO language with a notion of factories. C# had delegates much longer than Java had lambdas, and it also needs a way to talk about "a bit of code that produces objects complying with a contract", so also talks about factories.


Factories really exist because classes are not programmable in many popular OO languages as they are in Smalltalk. Languages inspired by C++ (Java, C#) tend to have factories. Microsoft used the unfortunate term "ClassFactory" in COM/OLE when they really meant one or the other, as it is a factory of objects just like a Smalltalk class.

Other OO languages don't have lots of explicit factories. I'm thinking Objective-C (abstract classes can choose the appropriate subclass for new objects), Python (packages tend to have instantiation functions), Ruby (very flexible), CLOS, etc. So the factory is really a design pattern that popped up to address a specific language deficiency caused by losing a lesson discovered in the late 70s (metaclasses).

Other design patterns are really quite useful and I don't see how they can be called deficiencies. Facades, for example, are widely used in OO and non-OO systems. Template method is duplicated similarly in procedural languages that allow dynamic dispatch, in any place where you need a policy that delegates parts of its fulfillment to a more dynamically selectable part. Iterators, strategies, these things have widespread use. Builder and Prototype are frequently used outside of OO. Adapter, Bridge and Proxy also appear in various ways without OO. Many of the other patterns are OO specific mainly because there is no desire to send a message to an object in other languages. For example, Flyweight is unnecessary if you don't send messages to objects.

Without OO, a lot of domains would end up with some sort of implementation of the same thing. OO itself is a pattern in this sense. You can write Javascript in a functional way, but it makes sense to encapsulate the HTML AST and not have its memory manipulated directly by the language. Does it make a lot of difference whether you put the node as the first parameter to a function or whether you use it as the target of a message? As some functions apply to only certain kinds of nodes, does it make more sense to document those functions by the list of known nodes that they apply to, or as a hierarchy of nodes?

People often say that inheritance is the problem, but you look at a successful (in terms of utility) object system like Smalltalk and it has quite a lot of inheritance. Refactoring leads to that. Sometimes a parent class might exist just to provide one implementation of a single method for two different kinds of object. Whether this is accomplished through inheritance, mix-ins, monkey-patching, protocols, multiple dispatch, or whatever, is somewhat immaterial. But when a language makes that difficult, and you have to copy-paste to get the same functionality in multiple places, that can be a problem.


Fair point I limited my view onto java. Other systems have various degrees of freedom.


I'm sorry but this is absolutely ridiculous. I sometimes teach programming to uni students, we make toy software like paint programs with a GUI in Java, networked game in C, that kind of thing.

I always have a good proportion of the class coming up with many patterns entirely on their own without any prior exposure, and I love their looks when I show them that the nebulous concept they came up with actually has a name and is well defined (and the best solution to the problems they encounter given the tools they have).


If a good proportion of an undergrad class is intuitively coming up with solutions that have some commonality, is that commonality really valuable? Observing it is neat, I just take issue with people treating design patterns as a northstar for quality software development


Design patterns as a concept exist without OOP. Even in procedural code or functional code you have concepts like a strategy pattern, or how do I get version X vs Y of object A. These things arise in any code you are writing a sufficient complexity.


My experience with the book and its patterns has been that there's an issue and some developers recognize that it could be best resolved with x, y, or z pattern. Then the developers go to extreme length to make sure the implementation adhere to the the 'principles' or 'spirits' of those patterns, so much so that it creates unnecessary complexity (e.g., unneeded layers of abstraction) and friction in other components (to accommodate the 'purity' of that implementation). What's worse, it's very difficult to challenge such implementation in code reviews, since it's originated by GOF, which automatically makes it legitimate. The worst part, though, is that you have to maintain the implementation, any deviation from those patterns is seen as corner-cutting.


> it's a book filled with solutions to imaginary problems.

Have you even read it? I'd say it has more deep real world examples than almost any other SW engineering book I've read.


IME it happens often that these categories overlap, when software gets entangled in incidental compexity.


This is basically a survey of a bunch of posts, and doesn't do much to provide a consistent critique.

Regardless, the true weak point of OOP is arguably implementation inheritance, which just doesn't leave you with a consistent semantics that's open to extension and changes in the basic/derived classes (that is, the well-known "fragile base class" problem is still a showstopper). But that has always been a pretty ad-hoc feature anyway. The other components of what people call "OOP", including encapsulation and interface inheritance. are all pretty well defined and not as prone to misuse.


I still don't understand why it's bad! Feels like spaghetti sentences tied together as a single article.

Why is OOP so bad, anybody? With scenarios, code samples or alternate implementations?


Inheritance in itself conflicts with encapsulation. First because it gives access to protected members of the super class. So it breaks encapsulation to the letter. Secondly because it hurts code locality so much. To such an extend it is difficult for the average programmer to determine what is going on by simple looking at the program. We have a recrutement test on this. Although candidates usually have a good intuition at what the test program does, it is amusing how easy it is to make them doubt by asking simple questions. Dynamic dispatch principle in itself is well understood but it opens so many questions about member access, non virtual methods access, overloaded method access to which people usually have no firm answer.


I think it’s hard to explain it because the very definition of OOP varies per person. Often people who hates it worked with a codebase that took some OOP principles to the extreme. E.g. I worked in a code base with extreme levels of abstraction where literally everything is a class.

That said, any principle including FP, taken to an extreme can produce a very tiring codebase (I’m guilty of doing this :p). So its not really OOP’s fault


In my opinion it is just the current anti-hype.

A few years ago OOP was supposed the be the magic bullet for everything, which it was apparently not. And now people critizice OOP for not delivering magic bullets. At the same time everybody seems to mean different things with OOP.

So no, it is not bad. Certain practices from OOP, like deep inheritance turned out to be worse in reality than expected in theory. But nothing dramatic. And by my definition of OOP - it is still a major part of every major language.


OOP is not bad. Also the entire discussion is pointless. Problems require solutions. You do the solution in OOP or not, nobody's gonna care, just that the solution solves the problem. Rest is just whispers in the wind.


There is a famous paper called 'out of the tar pit' which may be somewhat related.


I stopped reading after they said they don’t know much about what unit tests are really. It’s clearly not a well informed opinion, even if it happens to be right, which, from what I did read, it very well may not be.


I'm not sure that it is so bad.

But I still don't understand why it's good! Why is it so good? It is said to be better than (non-OOP) alternatives. Where's the evidence for that?


I would argue (my opinion) - OOP smashes together state + behavior. The original reason for this was good: I.e the user of the class(object) should not need to know ALL the internal details etc.

Something like ECS (data-orientated). Very explicit separates DATA(state) and BEHAVIOR (System, Functions, Methods etc).

For me at least it makes it easier and more reliable to "reason" and have a "mental model" about the big-complex system. If I know that for example in a game project ANYTHING with "gravity" is about the ONE Gravity-System(which runs 1x60 seconds in possible sep.thread) and that it Operates(change state) for ALL entities with the "Mass + Pos Components."


I always think the best argument for OOP is avoiding name space collisions - with procedural programs you have a bunch of functions that could be named the same - how do you separate them? OOP gives you the ability to wrap things together - their variables along with the methods that make sense for the objects - little collections of things that go together. Then once you have an object, sometimes you'll want to make a method work a bit different in different circumstances - inheritance solves this. Sometimes you'll want a bunch of objects that aren't really in an inheritance hierarchy to have some features the same - so interfaces (protocols) are born.


Isn't that just polymorphism? There are lots of ways to get that.



I think the major problem is that base classes are really two different interfaces (public and protected) combined with a default implementation, all exposed as a public symbol that anyone can reference.

So if you have a Vehicle base class with Car and Truck that inherit from it people will naturally externally do things like pass around List<Vehicle> and will extend functionality with Lorry : Vehicle and start using it. This creates a problem because you cannot change the base class of a Car or else it will no longer fit in a List<Vehicle> but if you modify Vehicle you may break people who inherit from Vehicle.

If you wrote it out explicitly without using inheritence though you'd have something a bit more sane and split up into the three different concerns:

    class Car : IPublicVehicle {
      IProtectedVehicle _vehicle = new Vehicle();

      public void Drive() {
        _vehicle.Drive()
      }
    }
Now you have a public interface where you'd create List<IPublicVehicle>'s and toss them around, but you would be free to write a VehicleV2 class that could be injected into _vehicle (you could even do it dynamically and go nuts with dependency injection). Then other people using Vehicle() wouldn't have their behavior changed and everyone would still live happily inside of List<IPublicVehicles>'s next to each other.

At the same time by having to write down the interface IProtectedVehicle (not shown) you'd be more likely to do actual design about the interfaces between your subclasses and your base class. This is the real problem which is that people do lazy shit design over the base class interface and just use it to shove shared code into the base and don't consider it a private interface and then go and break behavior as they mess around with more crap in there. And the "DRY" principle pushes software devs to ALWAYS push shit into their base classes, without even thinking beyond "because DRY", which is guaranteed to be bad design. With good design you may have to tolerate some level of necessary repetition in your base classes. Then you wind up realizing that you need derived classes that do not share behavior (e.g. consider adding a Boat subclass of Vehicle when previously you had taken the SinksInWater() implementation of Car(), Truck() and Lorry() and pushed that into the Vehicle() baseclass. Oops. Now your boats all SinksInWater() unless you override that. And if you override it you may be heading down the road of violating Liskov.

So that's the source of the objection that OO programmers will just tell you to write interfaces everywhere for loose coupling and only depend on interfaces not types (not abstract/virtual base classes).

But so far I don't know what Go or rust programmers do that is any different. And Go has a really fairly slick system for delegating interfaces to contained structs by composition which is just the same model I outlined above but where the two interfaces are the same. I'm still searching for where other programming languages produce a fundamentally better model. And as far as I've been able to ascertain Go's model is to just replace inheritance with interfaces and delegation -- which you can do in any OO model just by not using inheritance and writing interfaces and delegating. But then the complaint against OO is that proponents will just tell you to write interfaces, and for some reason that is bad. IDK if I'm missing something here...


> so far I don't know what Go or rust programmers do that is any different

Rust doesn't have object inheritance. So Car and Truck can't inherit from a class named Vehicle. However its Traits have inheritance, so for Car and Truck you could write implementations of a trait named Vehicle or a trait MotorTransport which inherits from Vehicle (and so you'd need to implement Vehicle too in this case as MotorTransport relies on that).

Rust only allows the author of Boat or the author of the trait Vehicle to implement Vehicle for Boat, so if you were to add a sinks_in_water() predicate to Vehicle either:

* As author of Vehicle you're responsible for implementing sinks_in_water() for Boats OR

* You've made a backwards incompatible change, anybody who uses the existing Boat can't upgrade to your revised Vehicle OR

* inside your own system where nothing external can force the version requirement, now it doesn't compile because Boat claims to implement Vehicle but it lacks sinks_in_water()


If traits map to interfaces and you have concrete implementations of those which can be composed into Car then rust is still just subtractive and eliminating inheritance. There's nothing you can do in rust then that you couldn't fundamentally do in OO languages by avoiding inheritance.

Which gets to the point of if we should be talking about avoiding inheritance specifically? Because "OO" is somewhat ill-defined.


Inheritance isn't gone from Rust it just only exists for Traits (akin to interfaces) as I explained.

For example std::cmp::Eq inherits from std::cmp::PartialEq - if there's an Equivalence relation between things of this type then necessarily there is also a Partial Equivalence relation between some of those things (specifically: all of them) so you implement std::cmp::PartialEq and then just say actually it's also Eq (ie this equivalence applies to the whole type).

If you make some types which you implement std::cmp::Eq for, and all I need is std::cmp::PartialEq, I can use your types, because of the inheritance. But the fact your types have std::cmp::Eq (and thus also std::cmp::PartialEq) does not prevent them from being quite different in every other respect to other types, nothing about the types themselves is inherited.

So this means thinking about inheritance in a different way but it doesn't mean the concept is gone from the language. A typical toy Rust type might implement half a dozen or more Traits, some of them inherited from others and some not, "eagerly" implementing common Traits is encouraged.

As to just not using subtype inheritance in languages which have that, you're likely to immediately run into an existential crisis when the language - not unreasonably - depends upon this feature in its own design. In Java for example you can't go anywhere without tripping over Object, the supertype of all user-defined types. Java expects you to use inheritance so avoiding it comes with needless penalties.


After quite some time with OOP, I rather just use it without inheritance (at least in business logic; inheritance is still useful in libraries).

In which case, it would be nice if the language supported ADTs


I've come around to Julia's point of view that it makes more sense for the methods to be separate from the object they're acting on.

I just wish Julia had a succinct way of saying "This object needs to have implementations for functions X,Y,Z", rather than duck typing everything and just seeing if it works. Maybe it isn't too bad in practice I just don't like it when a function can fail because the implementation changed even though the type signature didn't.

Anyway at the very least the approach of keeping methods separate helps to prevent objects that do not have any state and do not in fact represent any entity at all. Seriously who came up with naming a class "MyObjectHelper"? What on earth is it? It could represent literally anything. Does it even have state? And why?


There was a presentation pycon 2021 describing how protocols enables better typing in several cases: https://m.youtube.com/watch?v=kDDCKwP7QgQ&list=PL2Uw4_HvXqvY.... They sound similar to what you describe in that the protocol allows you to describe the features of the thing you need and the type checker can then help you determine that the thing you need has those features. Doing this kind of feature detection allows you to check ahead of time whether a call is likely to succeed but especially for a language like python you still need runtime guards of some kind to limit the impact of unexpected cases.


This is known as "multiple dispatch" or "multimethods":

https://en.wikipedia.org/wiki/Multiple_dispatch


Who is Julia?



> Performance approaching that of statically-typed languages like C

I might have to give this a try. Is there any language other than assembly that can beat the performance of C?


If you're programming an early 1980s minicomputer, almost certainly not.

On modern hardware, you will struggle to write large programs that have decent performance in C. It lacks a bunch of obvious intrinsics - things the hardware can trivially do, but which you can't really express in C, and so you end up maybe writing a bunch of macros to try to persuade your C compiler to emit the desired machine instructions. Now your code is harder to maintain, either you encapsulate all this, losing performance, or it gets very difficult to write more of the software and while your notional "performance" is good for the parts that work, the system as a whole doesn't work, so you don't have any performance.

Languages like C++ and Rust provide better intrinsics which means that actual human programmers can write the more sophisticated program that would technically be possible and just as fast in C except you'd never have written it.

As an extreme end of what's possible, WUFFS has much faster image codecs than are available as C libraries. But, WUFFS is under the hood just a transpiler, the output of WUFFS-the-language is horrible spaghetti C. So, in theory a human programmer could have written say, a C PNG decoder that's just as fast as the one in WUFFS-the-library, after all the C code is in theory code a human (a completely insane human) could write. But humans wouldn't do that because unlike the machine they can't keep a thousand step proof of correctness in their heads and be quite sure that variable can't overflow, they'd chicken out and write the overflow check and lose performance.


Other zero-abstraction languages like Rust and Ada have comparable performance to C/C++. Fortran in particular, which has a lot of high performance code written in it, is sometimes faster than C due to optimization assumptions the compiler can make about Fortran code that it can’t about C code. Julia can be slower than these languages due to having a garbage collector and not being a statically compiled zero abstraction language (though it is designed to dynamically compile to overhead-free code), but for some tasks it can actually be way faster than normal implementations in C or assembly, due to its ability to dynamically compile code specific to the particular invocation of a function.


GC overhead only happens if you allocate. Julia makes it really easy to not allocate. Also 1.8 has some features coming that are able to delete some allocations if the data doesn't leak through escape analysis.


Fortran, c, c++, rust, … are supposed to give you something as good as writing assembly manually . Generally, there’s no reason for a statically typed and compiled language to be slower than manually written assembly. In practice it depends on how good the compilers optimizations are and how skilled the assembly programmer is.


It's not about the programming language per se, but the compiler/linker optimization(s). C/C++ have this magic aura of being fast because it has the most worked on compiler/linker. Once other programming languages come around that are attracting more programmers, then theirs compiler/linker will be the best from optimization point of view. Also CPU's/architecture's change all the time so even if now C/C++ have the best on classic x86-64, that can change if a better CPU with a different opcode comes along. I believe this will happen in 10 years where we'll see quantum CPU's with massive parallelism implemented at which time the entire 50-ish history of C/C++ will simply become obsolete and every language will actually have to start from zero in terms of transforming human language coding into CPU's opcode.


The programming language certainly has something to do with it. If your programming language has things like garbage collection or dynamic dispatch, it's very hard to generate as fast code for the simple reason that your programming language is trying to do more things during runtime.


> It's not about the programming language per se, but the compiler/linker optimization(s).

As a wild example of that - just the other day I heard about BOLT in a podcast. It optimises the performance of LLVM/GCC binaries by moving around the object code after compilation. https://www.phoronix.com/scan.php?page=news_item&px=LLVM-Lan...


General purpose quantum processors are likely more than a decade away (assuming they're popular in the first place).

But vanilla C/C++ is already obsolete for performance - if you're writing high performance code it needs to be in CUDA, or some language / library that compiles down to CUDA.


Julia is quite hot right now.


I'm convinced the big win with OOP was more about modularity and encapsulation than OOP. The whole object.method() or object.variable model was pretty nice after spending years dealing with global soup or using naming conventions. The not-so-good part in particular happened when you inherited one too many times... very easy to write, and very hard to debug, maintain, and sometimes test.

A lot of OOP's success it was timing - OO showed up right around the time that we started building large gui apps. Now we have a lot of languages that do a great job doing encapsulation at the module level, which seems to be "good enough" and functional and procedural code seems to work pretty well, and be pretty easy to maintain at the module level.


This is my thought as well. There are still good reasons for using inheritance. In the right use cases, overloading methods can be a life saver. But generally, I have gravitated away from using those patterns, and toward things like composition, which are simpler and seem to be much less fragile.

Recently, I've enjoyed the simple utility of python's functools library. Small-inheritance-like functionality seems to find a happy medium in a lot of cases, while avoiding a lot of unnecessary abstraction and boilerplate.


"Why isn't functional programming the norm" discusses your points. If you haven't seen it, it makes a great in-depth argument for why you're right :)


Thank you for confirming my biases :-)


Back in the 90s OOP was touted as revolutionary. The next big thing, would completely change programming. If something wasn't object oriented, it was looked down upon. SQL even got on the bandwagon. It was said that very complex inheritance structures, operator overloading, and all this other stuff would (somehow) make it far easier to write and understand complex projects. Many seemed to have taken and repeated this on faith and not much else. I'd never seen any real justification for these assertions, it was never explained to me why those things would perform as claimed with sound reasoning, let alone actual data.

Were there ever any real objective [hah] studies done about how much it improved software development? And did they show a significant improvement? Even if you're still pro-OOP today, you would have to admit it fell vastly short of its promises even if it does help a little bit.

Today it seems like there's been very little accountability or learning from all this. Some people have sheepishly climbed down off the bandwagon, but there's been very little overall reflection. I'm not talking about witch hunts -- there will always be more snake oil salesmen -- I mean learning as individuals and an industry to demand data and reason rather than handwaving and assertions. The sad thing too is that a lot of the baseless hype came out of academia too (microkernels are another one that comes to mind).

I still see this today. The new languages and language features. New database concepts like NoSQL. "AI". Blockchain. All the way down to the CPU (transactional memory, various "security" features, etc). Proponents can make extremely compelling-sounding cases for these things, and make it sound like they'll solve all the world's problems. And some may well turn out to be a net win in the end. But the only thing that actually matters is the real world results, and you can only evaluate that by studying the data.

In general, if something sounds too good to be true, it usually is. Maybe the incredible trajectory of the computing industry has dulled peoples' common sense when it comes to detecting this kind of hype. It's absolutely rife in the computing industry and academia.


Wait, what’s wrong with NoSQL? It’s not good for shoving relational paradigms into, but it’s basically infinitely horizontally scalable, which, as far as I’m aware, isn’t possible with relational DBs, not at the same performance at massive scale, anyways.

A bit annoying when people shove a relational DB into a NoSQL schema though.


Why do you think it's impossible to scale relations (aka tables) to infitine scale? It is totally possible, just look at various analytical SQL-ish DB-likes (Apache Hive, Presto, BigQuery, Snowflake, etc).

Now, what's harder is to provide some of the stronger ACID guarantees, say, fully atomic distributed commits. Most of the time it's just a question of time it takes to reach full concensus in a distributed context.

But this has nothing to do with the relational data model itself, which is just tables of uniform rows referencing each other. Say what you like about SQL, but the core model is perfectly fine.


> Wait, what’s wrong with NoSQL?

For a few years back there, it was going to take over the world and we were all going to throw away 'old fashioned' DBMSs because they were slow, clunky and overcomplicated.

Like many of these overhyped technologies, when the dust cleared about 5 years down the line, we are left with something useful that definitely has its place, but isn't like wow huge it's taken over everything maaaaan. Meanwhile SQL is still with us and still good at what it does too.


I don't believe I said anything was wrong with it or anything else there. Most of the things I listed have their uses. That was completely not my intention to say they're bad, I hope that's not the point people are getting from my post.

The point is how uncritically some of these things get taken, and how easily people will believe fantastic, unfounded claims. And not just a few gullible idiots, but huge swaths of academia and industry.


Nothing is wrong with NoSQL except for how it (often) gets used. NoSQL is just a dumping grounds for less-structured data that allows startups to accumulate tech debt more rapidly, while providing enough functionality to be useful.

Where I've seen it used is to delay the decision making process of adding structure to data, or a prototype database, before you are certain what your application's needs are. For simple disconnected data in low performance applications, they provide a low barrier to entry. But eventually people start embedding foreign keys into documents and the whole thing goes South.


> A bit annoying when people shove a relational DB into a NoSQL schema though

This is what is annoying with NoSQL the same as it was with OOP and now with FP.

People learn this as the new better way of doing something mostly because they heard at a conference a FAANG dev sharing it and then everything should be built with it.

I saw a lot of projects where the developer(s) used NoSQL just because it was available or it was hot or it was what they learned in a bootcamp/article. But then they added relations so now a User has Projects and each project has categories and with constraints on relations and more ...and everything is glued together with NoSQL and suddenly they are reimplementing relational DBs logic in code with NoSQL being only a pure data storage.


> Were there ever any real objective [hah] studies done about how much it improved software development? And did they show a significant improvement?

I think years of hard experience across the industry found out that, for example, multiple inheritance and operator overloading caused more problems than they solved. Both features were taught and advocated back in the day, and now "there be dragons" signs have sprung up and most of the literature today warns the journeyman programmer to avoid them.


I've actually never really encountered issues with operator overloading. Is it just ADL, or are there any other canonical operator overloading issues?


It was abused a lot in C++, mostly because of weaknesses in other parts of the language.


Bullshit, it was the weakness in those developer's minds that screwed their use of this perfectly fine language feature.

This is a propaganda war, people. We are being told we are too dumb to handle knives. And the truth is, our industry lets incompetents play our roles, and we (those smart enough to use knives) must suffer the ramifications of those who stab themselves repeatedly and they cry out "it's the language!"


Is this parody?


Right. What I want to know is, what was the basis for claiming all this would be so great in the first place? It appears to have been almost entirely free of any evidence, as far as I've been able to tell.

It's mind boggling to me when we see the kinds of people in the industry and their demands for data and evidence when it comes to other subjects.


> operator overloading caused more problems than they solved

[citation needed]

Just because operator overloading can be abused doesn't mean that it isn't a massive boon in certain problem spaces (e.g. math libraries, SIMD libraries, etc.)


I mean, the standard way to do IO in C++ involves spamming the left shift operator (<<), I can only assume because it looks like an arrow? This is definitely a shallow thing, but I'd argue that for this single reason operator overloading definitely causes more problems than it solves (in C++), due to things like:

1. translating format strings to other languages is extremely difficult because the position of expressions in the message is fixed.

2. modifying how things are printed requires modifying global state, and it's easy to forget to reset the flags on std::cout after setting the precision of floats or something.

There's also the famous question of "what does the multiplication operator do on vectors?" problem, but that's something that could be solved by simply having a standard "vector" interface that defines it in a particular way. Overall I don't fully disagree, but seeing as it happened once with C++, I can imagine it can happen again in some equally widespread language (Javascript with it's + operator on strings maybe?).


> 2. modifying how things are printed requires modifying global state

This only applies to std:: cout and std::cerr. Other stream interfaces, like std::fstream or std::stringstream don't have this problem. Also, it is orthogonal to operator overloading.


I can pretty much guarantee that whatever the "OOP is bad" guys think people should do instead will be viewed as "bad" in a few years. I was around when OOP got into the mainstream and it started out as a very nice and practical approach. Then the ideologues came in and complained loudly "this is not OOP" so suddenly you had to wrap simple functions into meaningless objects. Then you had to inherit a lot and create these huge inheritance trees. And so on. Complaints that things got too complex were squashed.

The same will happen with every technique or process that becomes popular and consultants, authors and mediocre but loud people take over. Happened with OOP, Agile and will happen with other things too.

When I look at the Kubernetes, microservices, "need to scale just in case we may grow by 100000% soon" monstrosities we are building to deploy simple CRUD apps I don't think we have learned much.

People still take useful techniques, make them into a religion and push the techniques to the point where they are becoming a liability.


Interesting collection of point of views.

A language like Erlang indeed seems to align more closely with the spirit of the OOP goals - especially regarding encapsulation and avoiding shared state.

I would argue one of the best implementations of OOP is found in OCaml, where a nominal type system lives side-by-side with a structural type system, the latter which is used for objects and classes [1]. You still can use shared/mutable state, but functional styles are usually preferred - including a pattern known as "functional objects" [2], which allows the use of OOP but avoids shared mutability and its associated shortcomings.

[1] https://en.wikipedia.org/wiki/Structural_type_system#Example

[2] https://ocaml.org/manual/objectexamples.html#s%3Afunctional-...


> A language like Erlang indeed seems to align more closely with the spirit of the OOP goals - especially regarding encapsulation and avoiding shared state.

Indeed, Alan Kay himself said, to Joe Armstrong, that Erlang was closer to his idea of OOP than any contemporary language that claims to be OOP.


This is one of the things I love about OCaml. Even though the objects aren't used frequently, they were designed well, and people don't mind reaching for them when there is a need. It's a good way to show the "pragmatic, but careful" approach that OCaml has for a lot of things.


Yes, OO is about message passing. Erlang places a lot of emphasis on this, and in my experience one can use it to build huge systems that are easy to understand and to parallelize.


Conjecture: programming is hard, especially as the scope of the system becomes larger and the problem domain more complex. This means that all programming tools and techniques have at least some problems and downsides. It also means that any solution involves trading off various factors, e.g. code is fast or easy to read but perhaps not both. All this means that you can pretty much point to anything in our field and laugh heartily at how piss poor it is. You can do this with FP too. But FP isn't used so much for real world solutions <ducks> and therefore statistically you will find many more things to laugh about in OOP which is very commonly employed against real world problems.


>But FP isn't used so much for real world solutions

It's taken over the front end. The react paradigm is FP.

SQL read queries are FP.


> The react paradigm is FP.

https://reactjs.org/ homepage writes "Build encapsulated components that manage their own state" That's a textbook definition of OOP.


I use the term colloquially. The typical react pattern people use does not put state in the component. It uses a reactive pattern called redux and the state comes from somewhere else.


And, in fact, much of the front end ecosystem is constantly getting ideas from Elm which is pure FP… and a pleasure to use (I think).


I work with Elm and yes I can confirm this


This mirrors my thinking. Writing modern applications is hard, so we argue about _how_ instead.


> Conjecture: programming is hard, especially as the scope of the system becomes larger and the problem domain more complex. This means that all programming tools and techniques have at least some problems and downsides.

Yes.

> You can do this with FP too. But FP isn't used so much...

So close....

The problem is single paradigm tools. No single paradigm fits all of a problem. And in my experience, OOP is particularly bad at it. Not Prolog levels of bad, but also not much better. We have a huge number of very popular single paradigm OOP languages, because in the '90s the programming gods set forth the decree that OOP is the One True Way to design a system.


No single paradigm fits all of a problem. And in my experience, OOP is particularly bad at it.

The only reason OOP wins is because it doesn't enforce a single paradigm. You write 100% or 99% procedural code, you can write a functional program and encapsulate it. You can write a shell script and pop a GUI on top (it's marvelous in a horrible sort of way). OOP's lack of constraints is what lets it be used all sorts of places.


What are these very popular single paradigm OOP languages? The closest thing I can think of is Java.


Java isn't even single paradigm.


> Not Prolog levels of bad

Prolog is homoiconic like lisp, and support metaprogramming, so in theory there's nothing stopping Prolog from being great at most or all of a problem.


That is a wall of text that I admit I could not fully read or understand. It seems to argue that OOP is bad but what I gathered from it is that it argues that complexity is bad and that we should make things simple. The problem is that complexity is unavoidable and even if we make every part individually as simple as possible the final system will still be complex and hard to understand and test. OOP was supposed to help build the simple parts and then system would be easier to understand. It failed, not because it is inherently bad but because it enabled people to build more complex systems at which point the end result became hard to understand and test. FP might help a bit by forcing certain style of development which could make current systems easier to understand but the end result will be the same - it will reach the point where the systems built with FP style are hard to understand and test. What we should be developing is software to help us understand complex systems - both software systems and real world systems - and then the style in which software is developed will become less important and we will be able to understand how the system is working as a whole and drill into how individual parts are interacting. A good version of this software would allow more complex system to be created but it should also help with understanding them regardless of the level of complexity.


> I'm not going to go there much more because I still don't unit test my stuff. I'm currently not against it. I just don't know about it much.

Ouch, not even unit tests to catch regressions?

So many of the bugs that I deal with in $BIG_FOSS_PROJECT are regressions where a bug fixed in version X.Y.5 was reintroduced in X.Y+1.0 by someone's cool shiny new feature.

And yes, there's a significant lack of unit tests in some of these errors, plus some hard to test code. (While I'm happy I can use Mockito to mock static methods now, the fact that I have to is a very definite code smell).

Which is why I recommend never installing version A.B.0 of $BIG_FOSS_PROJECT. I always wait until everyone else finds the new bugs for you, then install A.B.1.


I’m not of any opinion on the matter, but with decades in the industry I’ve not seen a lot of projects where unit testing made a net profit for the company.

I feel that it’s important for me to note that this is because almost all the projects I’ve been around have been build in imperfect scenarios. Sometimes projects had years worth of terrible unit tests that were terrible because they were written by people who didn’t really know how to test correctly. Sometimes because they were added waaaaaaay late in the process. Sometimes because they were sometimes skipped due to time pressure. Sometimes because the pipelines weren’t really effective or set up correctly. And so on.

The issue I personally have with them is the same issue that I have with a lot of other dogmas around software development. All our theoretic tools are nice, if people actually adhere to them and know how to utilise them.

But as soon as things like Test Driven Developmebt, Agile, SCRUM, Enterprise Architecture or even Unit Testing meets reality, they break in most of the cases because the theories aren’t fascist enough. By that I mean is that you can implement things in so many different ways that almost nobody manages to make them work, not really.

This is anecdotal of course, and I’m sure there are much more talented people, teams and companies that derive a benefit from these things than what I’ve seen, but the only thing that’s ever, really, worked for me is too keep things as simple and single responsibility as possible.

That sometimes require OOP or Unit Testing though. We wrote our general ODATA API with inheritance as an example. We do so because it lets us have a unified way of handling auditing and code-first SQL database IDs and convention and because it lets us write a single API controller and then use it in, every, other controller instead of writing the same 90 lines or code 10.000 times.

You can likely do that without OOP but it’s just easy with OOP in C#.

So I see a lot of these things as, do what is necessary, what works, and, what is easily maintainable. If that’s Unit Testing for you, then do it, but I can assure you that it won’t be unit testing for a lot of people out there for whatever reason.

At least with single responsibility you still have a somewhat general idea of what goes wrong simply by where it happens if it isn’t caught but automated tests.


> Ouch, not even unit tests to catch regressions?

I also don't like unit tests and very rarely unit test. I think they provide a false sense of security and were invented by corporate software shops to better quantify "units of work" (oh, how many times I've had unit tests assigned to me in tickets!).

If you can't formally prove something doesn't break (in your head or with pen & paper or via pseudocode), your code is too complex. There are a few exceptions to this, but they are highly technical (e.g. FFT, cryptographic, physics/math implementations, tricky pointer arithmetic, regular expressions, etc.) where a hard-to-see typo can actually break things in non-obvious ways.

Most code is not that -- it's just written poorly (because code standards aren't enforced). Linux is a great example of a project where coding standards are annoyingly enforced (and there's no real "unit" testing) and lo and behold, the code is of exceptional quality.


I'd rather have an existing suite of tests that I can use to verify that when working with other people's code.

And more importantly, if JIRAISSUE-15295 is reproducible, then a unit test that a) reproduces it and b) verifies that the bug no longer occurs, is invaluable to prevent someone bringing JIRAISSUE-15295 back from the dead.

Of course, if the unit test is too tightly coupled, and has too many insights into code it shouldn't, then it's worthless as JIRAISSUE-15295 will most likely reoccur via a different code path.

But, poorly written unit tests aside, I have found significant value in unit tests when maintaining a rapidly changing code base.


A rapidly changing codebase is where unit tests are least useful though. Most of the changes are going to be because of new requirements which just means the test has to be updated. It's just busywork at that point.

If you have discovered a way to write unit tests that can tell the difference between a regression and an enhancement, please let me know.


This is probably a bad habit, but I've been writing tests so that I don't have to actually navigate though my app, put it in the correct state, and push a button to see if the feature works or not. Writing the test is just faster.


This is one of the major sources of bugs in my experience. People write unit tests that are perfect and pass, but only pass because you give it exactly the right input to make it pass in the first place.

Then when running the actual app, the data is not exactly like on the unit test and you have bugs.

To cover those scenarios, we use integration/acceptance tests.

So I always come back to the same question: then why should I even bother with unit tests? Especially the unit tests that people normalized (test per class/public method).

You end up with unit tests that are extremely coupled to the implementation, and without proper integration tests, you can't guarantee it will all work anyways. It only works on a bubble.

I believe tests should be much more about the broader behaviors than the implementation details, but TDD and evangelists of today will have you writing tests for every small class you create. I personally still use unit tests from time to time when there are a ton of edge cases on a single behavior that I want to test, but that's the exception not the rule.

By avoiding unit tests, or to put it differently making the units tested larger, you get more space to refactor, less coupling between tests and implementation as well as more meaningful tests.

Somewhere along the way we lost the meaning of "unit" and it became "class/methods", when it was originally supposed to be more at a module level.


I use unit tests primarily for a) verifying my edge cases (like the old joke goes, "a tester walks into a bar and orders -1, 0, jkhkhkhjkh...") and b) preventing regressions.


> This is probably a bad habit, but I've been writing tests so that I don't have to actually navigate though my app, put it in the correct state, and push a button to see if the feature works or not. Writing the test is just faster.

This is a clever way of solving this problem, and I've ran into it before, as well. Applications where state is very deep (like a game) and you need to verify certain operations on that state is quite tricky. IMO, I'd probably call these integration tests, not unit tests.


I've seen multiple people pointing this out, but completely ignoring the next sentence.

> I find it much easier to formally verify things correct than to test that they're correct.

I don't have anything else to say but I wanted to put it here for context.


Tests are also useful as a form of documentation. They explicitly exercise all plausible interfaces the developers expect to be used.

I'm not sure how formally verifying things continue to be correct after a change (which could be to a dependency) can be automated and scaled.


Based on my own experience, OO works best for small, self contained concepts whereas procedures work best for the overall program architecture. Notice when OO is evangelized it's always done using self contained concepts, like data structures or GUI widgets rather than architecture. When you do use OO for program architecture, like in Java, you'll end up with lots of pointless indirection and brittle, over engineered design pattern soup. Procedures are simply more "fluid" because they are smaller, self contained and easily refactorable which is exactly what you want when it comes to program modifications.


The PersonnelRecord isn't OOP and exhibits a widespread misunderstanding:

    class PersonnelRecord {
    public:
      char* employeeName() const;
      int   employeeSocialSecurityNumber() const;
      char* employeeDepartment() const;
    protected:
      char  name[100];
      int   socialSecurityNumber;
      char  department[10];
      float salary;
    }
As written, PersonnelRecord class will inevitably lead to code duplication, tightly coupled classes, and other maintainability issues. An improvement that's still not OOP, but exposes a more flexible contract, resembles:

    class Employee {
    public:
      Name name() const;
      SocialSecurityNumber socialSecurityNumber() const;
      Department department() const;
    private:
      Name name;
      SocialSecurityNumber socialSecurityNumber;
      Department department;
      Salary salary;
    }
OOP is more about the actionable messages that objects understand to carry out tasks on behalf of other objects. Wrapping immutable data exposed via accessors reaps few benefits. Rather, OOP strives to model behaviours that relate to the problem domain:

    class Employee {
    public:
      void hire();
      void fire();
      void kill();
      void raise( float percentage );
      void promote( Position position );
      void transfer( Department department );
    private:
      Name name;
      SocialSecurityNumber socialSecurityNumber;
      Department department;
      Salary salary;
    }
This allows for writing the following code:

    employee.transfer( department );
I don't know how to "transfer" an employee given the code from the article, but it would not be nearly as elegant.


I dunno man... I think that giving an entity the ability to perform operations upon itself and thereby changing it, looks neat in these examples but scales poorly in a sufficiently commplex codebase. You could express exactly the same thing but get a lot more bullet proof code if you separate out the thing doing the action from the thing you are acting upon. You could have the thing doing the action return the updated version of the thing you wanted to do said operation upon and its unambiguous what has happened. So something like (psuedocode):

    transferredEmployee = employeeService.transfer(employee, department);
IMO this forces a certain style of coding that ages better and requires keeping less stuff in your head.

My two cents. Have a good one!


Sure. Depends on the problem domain. One could equally write:

    xferEmployee = employee.transfer( department )
    xferEmployee = company.transfer( employee, department )
    xferEmployee = department.transfer( employee )
    exEmployee = humanResources.fire( employee )
Or, using an event-based architecture:

    new EmployeeTransferEvent( employee, department ).publish()


Your event approach reminded me of Eric Lippert’s blog post: https://ericlippert.com/2015/04/27/wizards-and-warriors-part...

He also ends up with the commands/actions/events/rules (basically emphasize relation over object) as main types in an interesting[0] example of a rule-based game.

One of my issues with “mainstream OOP” (at this point I’m not even sure what “real” OOP is) is that it apparently tempts people to base their models around objects and subjects not verbs.

I’m not sure if it’s due to prevalent nominal type systems or due to how we traditionally teach class hierarchies (dogs/cats <- animals) but I think centering the model around predicates and relations works much better. [1]

[0] Interesting because while it is a toy example it is one that could be real; not like examples with cars and animals.

[1] Just reminded me a bit of Wittgenstein’s Tractatus; didn’t want to get too philosophical but I think the ontological views we have influence this kind of modelling a lot


Why not department.transfer(employee) or workplace.transfer(employee, department_from, department_to)? I think that sometimes the most OOP way is just to write

    struct PersonnelRecord {
      char  name[100];
      int   socialSecurityNumber;
      char  department[10];
      float salary;
    }
and be done with it.


Classes are fine so long as:

- They're small and single-purposed

- They don't inherit (or at the very least, extremely light inheritance)

- There's a very clear relationship between the state and the methods (e.g. a counter class, or a future/promise)

For example in JavaScript, I think you can still uses classes and end up with functional-ish code: https://bluepnume.medium.com/functional-ish-javascript-205c0...


Here is a different take: OOP is not evil. State is not evil. They exist for a good reason:

Procedural is "linear" while OOP is "layered". One could write code either way but OOP better resembles the physical world and since brains have grown up in the physical world, layered code is easier to grok. It can hide/show pieces such that you can inspect functionality in a controlled way. Of course procedural can do the same with functions but it's coarser.

State is also a property of the physical world. Things persist, stuff accumulates, that's just how it is. Since applications ultimately model things in the physical world, they also have to persist things and accumulate things. State is inevitable.

OOP is a sharp tool and of course it's possible to use it well or cut one's self really badly


> OOP better resembles the physical world and since brains have grown up in the physical world

The part about OOP which doesn't resemble the physical world though is encapsulation. Encapsulation is like pretending the world functions in passive voice, with objects doing things to themselves.


OOP is a great fit for UI frameworks. OOP is a bad fit for many other things.

They guy who hammers nails all day thinks screwdrivers are worthless.


John Carmack came to the opposite view in the end (https://web.archive.org/web/20130819160454/http://www.altdev...), favouring pure functions and immutability above all else.


OOP and functional programming are orthogonal -- you can do both ...


That's the exact premise of Scala, by the way: more object-oriented than most object-oriented languages (including Java), and yet enabling functional programming.


OO gets you a long way, still there are interesting approaches being tried with data-driven programming through either composing lenses and ECS. I have a hard time understanding lenses, seems like a lot of work to reproduce the most strait-forward concept in OO (getters and setters). ECS do away with object and give you almost like a relational database, but it is cumbersome to encode hierarchies, but they do offer a way for non-code tools to interface with code which is important too.


Lenses solve a problem you don't have in OOP. Since OOP allows mutation, to change a value within a nested structure, just mutate it. If you don't want to mutate, then getters and setters don't help and changing nested structures is awkward.

Lenses solve the nested mutation problem for immutable structures but then go past what you can do with getters and setters. For example, you can compose bigger lenses out of smaller ones.


If ECS' Systems are querying Entities / Components the way one would use a SQL query in a relational database, what's needed for hierarchies is the ECS equivalent of a graph database.


The good parts of AppKit (which most OOP frameworks are based on) wasn't just that it came with nouny objects representing controls, it's that it used the "hard and soft layer" pattern with messaging and notifications. Which is not C++-style OOP.

Most of the even more OO frameworks around the same time, like Taligent, failed.

Of course it did win over procedural frameworks like classic MacOS, but some of those are still popular in games or embedded systems.


> OOP is a great fit for UI frameworks.

The biggest UI framework of the past decade is decisively anti-OOP in its philosophy


I assume you are talking about React. It started fairly OOP. Now it's... something else? It's not functional. IMHO hooks and effects are a crazy form of OOP. It's like some weird dynamic scope data definition.


I'm not exactly sure how hooks are related to OOP. Their design was inspired by 'Algebraic Effects' which are more functional in nature than OO.

This gives a good breakdown: https://overreacted.io/algebraic-effects-for-the-rest-of-us/


Ah, that does make more sense, and is a language feature I've often thought about. It's usually just a pattern and not a first class feature, but with many problems as a result (many of which React itself encounters).

Lots of use of context managers in Python are for accomplishing this. Smalltalk error handlers are like this (different than try/catch). Or Scheme's with-output-to-file... I got the sense that variables that start and end with stars in Common Lisp are used for this, but I was never clear if it was convention or an actual language feature.

I think I'd like it in React too if they just hadn't tried to be so clever.


Isn't this basically the same tradeoff as Contexts vs Prop-Drilling?

You could achieve the same effects by passing down callbacks.

I thought the reason why this is good for Errors, Themes and Loading (Suspense), is because they're so common, that you can always expect someone up in the hierarchy to consider it, and therefore sacrifice some type safety for less verbosity.

The article mentions that "usually the fact that a function can perform an effect would be encoded into its type signature", which is why I don't understand the need in this example.


Honest question, I’m very curious. Do you still classify React as closer to an OO paradigm than functional?

Agreed hooks are… something else.


Running a function and then looking for magical side effects to effectively get multiple return values from that function sure isn't functional. And it shows in how unintuitive it is to reason about hooks and the leakiness of the whole thing.

I think it looks functional because it's only easy to reason about if you write functional code.

I suppose it tries to solve some of the same problems as OO, like related state and private transitions. But it feels like React itself is the God Object.


What it the name?


React, presumably, with its approach that a UI is well described by nested functions operating on state. It's a good point. I remember long discussions on mailing lists for various FP languages conceding that UI toolkits were the one area where OOP seemed the natural fit. Haven't seen many of those discussions since React emerged.


>" nested functions operating on state."

Switch it to a state with the methods and you got your object. Polymorphism here is not a requirement. It is a feature to be used when needed. Some time in the 80s I was doing exactly the same - operating on state with functions. Same shit different color.

Programmers just love going on crusades. I find it a waste of time and intolerance breeding ground. Instead of heating the air use whatever the fuck suits one. Just do not stalk other people who happen to have different preferences.


Ah that one. Sorry but in my book React is an abomination. Speed of developing decent UI in React vs for example something like ages old Delphi is incomparable. The last one wins hands down.



React


I'm currently working on an issue thats taking me longer than it should (I'm a slow). It's a change to a json structure that impacts multiple files in lines that consist of 5+ method chains that I have to track down in other classes and custom libraries to see what it's doing (Java).

It could have been a single Python file using Pandas to build a DataFrame and this change would have been so much easier. And I'm saying that as someone with at least 10x more Java development than Python development.


A fool with a tool is still a fool ... to build quality software, apart from understanding the problem to solve and putting one's heart on it, there is no other way.


People spend way too much time arguing about this.. If you're a good programmer u will be able to do excellent maintainable work in any language you're experienced with. If you're bad you will make a mess in any language.

It'd be like carpenters saying "spruce is terrible if you make anything from spruce its fucked, you have to use oak." ; A good carpenter will be able to make something amazing from spruce.

Like carpentry, metalworking etc. the quality of the product of programming is heavily based on the skill of the tradesman.


Experience is not highly valued in articles about programming but often it comes down to that. I'm using tools with roughly the same level of expressiveness as I have for the last 20 years but I develop much better code now.

I've been guilty of over-abstraction and I've been guilty of under-abstraction.

There's a lot of blaming the tool for what amounts to lack of experience. Everyone has lacked that experience at some point in their career.


Yeah pretty much this.

If a paradigm (whether functional, OOP, or whatever label makes you feel better) isn't working for you when it has worked for many others, maybe it's not because the paradigm sucks... it's because you might just suck at the paradigm?

Obviously there are cases where things were pigeonholed into the wrong paradigm for the job. Then it's a case of the person choosing the paradigm sucking at choosing the paradigm. Whatever happened to personal responsibility?


Absolutely. I really want to love FP style programming, but I just can't get it to gel in my head. I'm totally on board with the concepts of side effect free programming, minimising state, passing around functions as parameters, etc...

But I struggle with reading it, and I struggle with writing it, and I struggle to build a conceptual framework of how FP style components fit together. I've been banging my head against the FP wall for _years_ now, and I still can't seem get a level of fluency that where I'm comfortable using it regularly.

That doesn't mean FP sucks, it just means my brain isn't wired that way.


You don't understand. This is not what FP programmers are arguing about.

What FP programmers are arguing about is that all code becomes shit if you do OOP. It doesn't matter how good you are.

They argue that if you do FP your code is much less likely (note the word likely) to be shit.

The analogy to carpentry or craftsmanship is bad. Programming is about managing complexity to a degree no one can fully hold in their head or understand. You can't just be a "master" craftsman and write an entire OS with zero technical debt.

The argument here is that if you use FP over OOP your code will have magnitudes less technical debt.


It sounds like you don't understand about various crafts. To me they are a quite analogous to programming.

Also, code has barely changed at all since C. It's all just some variables, loops, flow control, some math operations and doing stuff with strings sometimes.

One can write functionally in an OO language. No paradigm or design pattern or whatever will save you from making a mess. For me, the more experienced I've become, the less those things matter.


> It sounds like you don't understand about various crafts. To me they are a quite analogous to programming.

I understand every craft. They are analogous in the simple minded way you're thinking about it. However complexity in these crafts does not rise to the heights like the way it does in programming. That is the key difference.

>No paradigm or design pattern or whatever will save you from making a mess

This is fundamentally backwards. The FP style saves you from all errors related to mutation. This single statement already proves you wrong. You can't mutate a variable so that's one mess literally taken off the table. There are patterns that can provably prevent errors from happening and you can create languages that strictly enforce certain patterns.

Elm for example, cannot have runtime errors. The language strictly prevents that "mess" from happening.

Outside of strictly enforced patterns the FP pattern promotes better organization. You can still make a mess. But you are less likely to make a mess. Additionally for extremely large programs the proportion of technical debt will be significantly less for an FP styled program than an OOP styled program. See below for one instance of this.

http://wiki.haskell.org/Why_Haskell_just_works

>For me, the more experienced I've become, the less those things matter.

I've found that people who brag about experience doesn't make them smart or good programmers. You have limited intelligence and no matter your level of experience there are just design mistakes you make all the time in extremely large programs. Technical debt is an unsolved issue. Though the claim here is that FP programs tend to have less debt than non-FP programs. The keyword is "tend" as there are exceptions but the generality is mostly true.


Sure your first proposition is true. But many people realize you more than likely will not have a team of good to great programmers.

OOP is a massive foot gun for many programmers who are okay at best. And that effect is exponential as the project and their team grows.


What isn't a footgun in the terms that you declared?

I believe people dislike OOP because the vast majority of code is written using OOP. If everything were written using data oriented programming or whatever, people would have the same reaction as they have regarding OOP.


>> What isn't a footgun in the terms that you declared?

There are definite degrees to this. But something like FP that declares f(x: X) => Y, instead of something like OOP where given context: x: X, y: Y and f() => void (where there is an effect on y) those are massively different scopes of problems.

It's not that you cannot do the previous in the latter, but that people -- especially those that are inexperienced and haven't dealt with the hell of a stateful programming -- routinely reach for as a solution to their problem.

>> I believe people dislike OOP because the vast majority of code is written using OOP.

Why do you believe that's the reason why? There's no implication here -- just a genuine curiosity as to how that was the conclusion.

>> If everything were written using data oriented programming or whatever, people would have the same reaction as they have regarding OOP.

DOP especially given one with a static language forces you to move from one type to your next to solve your problem, and handle your edge cases.

From experience DOP and FP can be intimidating, but usually end up being easier, and the development time, along with the time to stability are hugely reduced -- because it's inherently dumb.


So, just to clarify before people think I'm an OOP advocate and it's perfect, I don't, but I think it has it's place.

> There are definite degrees to this. Agree. The question is, does OOP leads to more buggy code? Maybe yes, so we ban OOP? Because that's the sentiment I get whenever someone is against it. In your example, is it ok allocating a new object every time? If you can't afford to do it, what do you do?

> Why do you believe that's the reason why? I think in terms of proportions. For example, if you have 80% of all code written is OOP, chances that you'll find a bad OOP is greater than finding bad code in any other paradigm.

What if people learn programming using FP or DOP? There would be less buggy code? I don't know, but I think in the end, OOP would become the popular choice because, in my opinion, it fits very well the way we think about our world and we would end having this same conversation.


The problem with OOP is not just with poor implementations and inheritance models (e.g. Java) but that state encapsulation is actually bad and cements complexity.

The real goal should be to untangle state and decouple it from computations by putting it all in global shared state (e.g. SQL, Redux, ECS frameworks, etc.) while using stateless functional computations to compute changes to the global state. Pure functions are composable in a way that objects simply aren’t.


The opposite end of OOP is the functional purist world where "color = blue when hovering over button" becomes 10 levels of boilerplate indirection in order to update global state and receive changes using actions/reducers/containers/memoizers. It's just substituting one type of needless, brittle complexity for another.


Exactly and the functional way is harder for humans to grok


That’s fair and as someone who has that same frustration with it I realized Redux might not be the best example precisely for this reason. I’d argue though that the problem here is the premature abstraction of encapsulating state in actions/reducers/memoizers. This sort of thing would be way more natural in an ECS framework.


Functional purist here (mostly Elm), and I have very few problems that arise from the paradigm. Also fewer bugs than pretty much any software I use or deal with.


How is state encapsulation bad? Encapsulated state means that only a very few functions can access/change that state. Non-encapsulated state means that any code in the entire executable can change that state. How is that better?

> The real goal should be to untangle state and decouple it from computations by putting it all in global shared state (e.g. SQL, Redux, data oriented game frameworks, etc.) while using stateless functional computations to compute changes to the global state.

I kind of presume that you're in a single-threaded world. Shared mutable state plus multithreading is a recipe for disaster.


In functional languages you can still ensure that only certain functions can change certain parts of the state. The big idea is that state is isolated. You can actually write functional code that looks a lot like OO code (essentially just calling `foo(thing)` instead of `thing.foo()`) but because state is isolated, you know that nothing is changing behind the scenes. It may not seem much, but it's actually really freeing to not have to think about that stuff anymore. Same thing with immutability.

I was skeptical of functional for years and was actually a big OO zealot. I finally brought myself to give functional a serious go over 1.5 years ago. I now write it professionally and don't miss OO even a tiny bit. It's really just something you have to try and have an open mind about. I actually still find myself sometimes thinking, "Crap, maybe I should make a copy of this in case something else tries to touch... OH WAIT! IMMUTABILITY! I'M SAFE!" It's a really nice feeling :)


Let me separate out immutability for a moment.

If I don't have immutability, then if I can call foo(thing), then I can write bar(), and then call bar(thing). Bar can now alter thing. So my encapsulation can be broken by someone just writing a function. (This is the same problem that C had with structs - anyone could write a function to alter the data in your struct, and thereby put it in an inconsistent state.)

Now, with something like C++, you can still do that. You have to go to thing, though, and write a new thing.bar(). It therefore becomes much clearer that bar() may break the consistency guarantees of thing.

So I don't think that just functional gives you the guarantees that OO encapsulation does.

Now, immutability changes things... a little bit. But with immutability, the problem only moves, it doesn't go away. Someone can now write a bar() that returns a new/altered thing, and then pass it to me. I can still get a thing that is in a state that violates the rules for what a thing is supposed to be.

And, once again, the same thing can happen with OO. It's just that, if it happens, you have a lot less code to look through to try to figure out how and where it happened.

You may have noticed that I care a lot about data being in a consistent, valid state. If you don't care about that, then my arguments may not resonate with you.


You can’t separate out immutability because without immutability it’s not functional code, it’s just procedural code.

To the question of consistency though, OOP gets you very little because consistency rarely maps directly to objects. So if you end in a situation where object A is inconsistent with the object B, you still have to trace out all the locations where object A and object B might have been mutated and figure out what combination of code causes the issue.

At least in the global state system you can get runtime consistency by running consistency checks on each attempted transaction.


There are two types of "data" in a properly encapsulated class [1]:

1. Internal/private data -- things like a pointer to a string's contents, its length, and the capacity of the content buffer.

2. Public "data" (API) -- best provided as accessor methods/properties (and public methods) to ensure that the class' invariants hold if these can be used to mutate state. You don't care if the object has been mutated through this public API as this is the contract for how the class/object instances should be used.

A properly encapsulated string class is free to change its internal state/data (e.g. start+length+capacity, or start+end+endOfBuffer), as long as it keeps the contract defined by the public API intact. The same applies for data structures, mathematical objects like complex and rational numbers.

You could store a complex number in polar (r, theta) or cartesian (x, y) form and provide public accessors for all of those values. If you had setters for those, the native representation would be a simple assignment, while the other representation would do the necessary polar <=> cartesian conversion. This would maintain the invariant that the two representations are equivalent, such that if you set r then the angle (theta) does not change, but the magnitude changes such that it is equal to r.

Note that if the complex number (or string) was modelled in a procedural or functional language, you would have to choose and stick to one or the other representation. That structure or type definition is leaking the state, such that a program could modify the length of the string without updating its contents.

Q: Can you give an example of where having object A inconsistent with object B is an issue? That would help me to understand the issue/problem you are referencing.

[1] Many OOP languages also allow protected data, which can be seen by implementations but not any other code. These are risky, as they can allow the derived class to break any invariants on that protected data like you described in your second paragraph. As such, I try to avoid them wherever possible in my own code, but will run into them in thirdparty or system classes.


Your last sentence there is pretty unnecessary. Moving along...

Echoing my sibling comment: immutability is a defining feature of FP, so there is no way to put it aside if we're talking about FP.

As for the rest of your argument, I'm a bit of meathead and maybe haven't followed it fully. My thought is that in OO, an instance of an object can be passed some data that it processes along with its own internal state. If that data is a reference to another object, then that object could be changed while the receiving object is doing its thing. The receiver then would go and store its results within itself.

In functional, functions receive everything as parameters and all that data is guaranteed not to change for duration that the function runs. Once it returns, it will update the global state and then, again as my sibling comment points out, it's far easier to check validity of the whole shebang as it's in one place.

As for "you have a lot less code to look through", I feel this is a common thing said by folks who have not really grokked how FP works. I say this because I used to make this argument ;P While FP can be slightly more verbose than OO, I've found FP way easier to figure out where things came from.

All in all, I don't have a vendetta against OO. I was really just trying to get across that I was a big OO enthusiast and went in FP perhaps even with a closed mind and it didn't take much to win me over. Having said that, I'm in the Elixir/Erlang world, which is a bit of its own beast.

(Edit: your/you’re/yore)


I also want to say that I'm in no claim that FP is a silver bullet. All-in-all, programming is terrible, lol (naw, I love it). I've just found myself carrying around fewer footguns since I converted and generally less stressed and confused by what's going on... even when reading less-than-ideal legacy code.


> Now, with something like C++, you can still do that. You have to go to thing, though, and write a new thing.bar(). It therefore becomes much clearer that bar() may break the consistency guarantees of thing.

Well, no it doesn't. I've worked with too many codebases that has a declaration of `void foo (sometype_t &instance);`. Then when you read the code and see a function call of the form `someFunc(localInstance);` you have no idea if someFunc can change localInstance.


Encapsulated state is bad when it encapsulates the wrong state. The wrongness becomes less visible, but appears more correct, because by mere assertion the encapsulation can boldly define the correctness of something wrong. In novice hands that's very dangerous and long-lasting and is the unsafe pointer equivalent of architectural design.


Are you saying we should remove local state? If you aren't then why is that better organized than object state?


Few people are organized and use encapsulation to store things where they are easily retrieved. Most people are messy and encapsulate to hide junk under the couch.

It's the code monkeys, stupid. Given a powerful enough machinery most code will look like junk. That's why some "beautiful" languages today tie your hands and dumb everything down so you aren't free to do as you like, whereas assembly was just fine 40 years ago.


We’re past this turf war. The original “case against oop is overstated” is an attempt to go beyond the gap. It was an article that said “sure oop has problems, but it’s not all bad, maybe we don’t need to throw it all away”.

And then there is this article who just goes through more articles trying to re-ignite the debate. It’s silly.


The truth is that neither FP or OOP is the silver bullet.

Both have good techniques, that shine for certain types of problems.

The more important thing is how you do it, how good you are at it.

It's the same thing with e.g. Agile, some people make a mess of anything including agile, some people do it well, and the vast majority of average people get average results with Agile.

So don't waste time getting stuck on one particular school of evangelism, try a range of techniques and get good enough at all so you can match the ideal technique to the specific solution you're creating.


I've come to like "service-oriented programming" we use at work (I don't know if there's a name for it). Basically, your code is essentially procedural, but instead of free standing functions you use stateless service classes hidden behind interfaces: it gives you very granular namespacing, implementations are swappable, composable, decoratable and testable.

In this system, the models (records) in the domain model are more like classic OOP (objects like "car", "banana") and they have encapsulated state, but only to have a centralized place where you make sure object invariants are upheld whenever their state is mutated (by services), and nothing else.

The hard part is interoperation/invariant validation between multiple objects (aggregates or just related data) and we haven't solved it yet in a consistent manner (everyone comes up with their own approach, for example state propagation via events).


Isn't this Domain Driven Design? I think you understated how easy it is to test in your example. Tests can move from CD to CI because of your architecture. I would love to see more discussions about the tension between scale and test-ability.


We tend to judge OOP by the worse way we've seen it used. To really be fair, we need to find the best usages of OOP and see how those have faired over time. I'd suggest looking at NextStep as it seemed to be created by a team with a very high degree of expertise in OO as well as a strong design focus.


I have no idea about the points that post is trying to make.

OOP like everything, using it incorrectly will lead to trouble, and it has its merits when done right. plus, it's used widely in practice that is really the best evidence in that, it's not bad at all.

use FP all the way IMHO will lead more spaghetti code, use it to complement OOP could be great however.

Last, what's the alternative? if you don't have a better alternative, you don't solve any existing problem.


We have data and complex systems. No one, with the exception of Alan Kay, has convinced me that Object Oriented Programming is or was a good idea.

… Because the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.“ —Joe Armstrong, creator of Erlang programming language


But, thats exactly the reality of the problem, and exactly what you want. I work with robots, so much of it is physical. There are no floating bananas in the world. They're all held up by something.


The key that Joe was trying to make I think was that the forest and the gorilla have to be explicit:

    State2 = update(State1, ArgX),
    State3 = update(State2, ArgY),
As opposed to, say:

    obj.update(arg_x);
    obj.update(arg_y);
obj holds a gorilla and the jungle, and it may be hard to know how update method works because there is a complicated diamond shaped class hierarchy, and then a thread may concurrently modify parts of it behind our back. You're just trying to add a method or fix a bug, but it's almost impossible because we don't know it affects the gorilla and the whole jungle.


The problem is that setters are usually void functions. Instead they could return the new state/object

State2 = obj.update(argx)

State3 = State2.update(argy)

But if you do this people will argue this is inefficient, because you are copying a lot of objects.


> But if you do this people will argue this is inefficient, because you are copying a lot of objects.

Syntactically it is annoying to some extent. But, just like the most important data gets saved to a database with lots of "ceremony" involved -- separate protocols, transactions, SQL statements, etc, because it's pretty important to track updates carefully. Here ,it's a bit like that, but on a smaller scale as in memory program state is also important, and has to be tracked explicitly, and for that some "ceremony" is acceptable.

Implementation-wise, because of immutability, there is copying but it is often not 100% duplication. The updated version and the previous one behind the scenes (in heap) might share a lot of common structures. For example, if we have a 1000 element list L, and we updated it with a new element at the front L1 = [H | L], then L1 and is not a complete copy of L, but instead is just element H and a pointed to the shared tail L. For dictionary data structures (maps) something similar happens but there it's a O(log n) order of updates with everything else being shared. Definitely not as efficient as in-place updates in say C++ or Java but that's a price I'd happily pay.


I don't really understand arguments for or against OOP. OOP, to me, has always been about blackboxing some data entity and interacting with it through an API. Where it got wild and wacky was inheritance, but there's nothing fundamentally bad with having a process or object with an internal state you interact with via an API, e.g. a microservice.

Most computer programs have an internal state that you interact with through some API. And sometimes you need to compartmentalize a process and have it work independent of another process. There's literally no way to get away from programs with an internal state and API access since that's how computers, and electronics in general, work.


There's a reason most complex software uses OOP. Functional programming is nice, sometimes, but as projects grow it ends up using patterns that do essentially the same thing as OOP. A monad is basically an OOP object.

To see a language that is purely OOP but also forces you to be very explicit about accessing state, check out Pony language.


> A monad is basically an OOP object.

i think it's the other way- an OOP object is a poor imitation of monadic algebra that cannot be generalized with mathematical rules to guarantee certain properties.


How I've seen OOP work at big tech is the following:

1. New code base is needed

2. Some developer comes up with the master oop abstraction to solve the problem

3. Years of dev effort spent with the abstraction at the core

4. Dev on step 2 is now the expert in some overly convoluted complex system

5. Is determined a genius since new comers cant easily grasp the complexity, gets promoted to a high ranking dev job

My number one rule of coding politics:

He who creates the OOP abstraction rules the team


My grading criteria for a codebase is "Can a hungover junior engineer comprehend this and work on it successfully?"

When we are getting to "only a Principle engineer who is an expert in scala with a history of strong mathematics can work on this" is about when a codebase gets a fail.


Thats a broad generalization and many niche codebases cannot be high quality and still have a drunk junior become productive with it in a week. People need to accept that it's ok to require for some teams to have discussions on a level which requires serious experience and education


Indeed. A more nuanced interpretation of the question might be: “Can the most junior (or least tenured) engineer on my team understand the code in time to deliver the feature they’re working on?”

To be fair, OP’s original phrasing is still a good rule of thumb. And nowhere does the OP imply it’s an absolute.


This does a massive long-term disservice to junior engineers, especially because they're most likely just out of school and still very eager to learn.


Merely being interesting or rewarding to work on doesn't justify complexity though. I think it's a reasonable mindset to approach things with in an attempt to simplify things whenever possible.

Your entire team could be highly experienced and have PhDs in exotic subjects and I still think it would be a good approach to take.


> Merely being interesting or rewarding to work on doesn't justify complexity though. I think it's a reasonable mindset to approach things with in an attempt to simplify things whenever possible.

Yes.

The simplest code isn't always the most immediately approachable though. At least due to the way universities and bootcamps teach things. OOP only smears complexity around and should be avoided at all costs, but the "simple" alternatives that most people have in mind (usually procedural Python/Go) aren't much better. Simplicity shouldn't be synonymous with repetitive, error-prone nonsense like for loops and null checks just because they are familiar. Junior engineers should be schooled up on mapping, folding, optional types, ADTs in general, etc, because these things make code radically simpler.


> with a history of strong mathematics

TBF that one is really going to depend on the domain addressed by the code.


This pretty much happened at one of my jobs. Principal engineer was airdropped into team and decided everybody would develop all new microservices with his new pet framework (literally, it was named after his dog) with Spring style DI cruft that was in many ways more difficult to work with than Spring. He enjoyed rulership of the team for a while before leaving for greener pa$ture$, and on his last day of work, he changed the README on the open source repository of the framework to point to his own, personal, repo as the locus for all future development. A move widely frowned on, especially by management and legal, which soured his reputation, and higher-up architects had different ideas anyway, so the pet framework was deprecated for future development.

Lesson: If you're going to code your way into being the only developer who understands the core abstraction behind your employer's critical code, do not overplay your hand.


I agree, but I could also say the same thing about functional programming. If you let the dev that won't stop talking about monads touch your javascript code base it will soon become a tangled => web => of => arrow => functions


Regular non-functional JavaScript code is riddled with non-arrow freestanding functions, is that any better?


Probably, because you step through it in a debugger without bouncing all over the code base for each expression.


I don't have a problem with arrow functions, just saying you can make a huge mess with OOP or FP.


What does OOP have to do with any of that? I was at a dev shop that used no OOP, and everything happened the same way.

:-/


Those who hate OOP have never used OOP correctly.

None of these articles mention 'cohesion' or 'coupling'... It's not possible to critique OOP unless you understand the principle of loose coupling (ease of substitution) and high cohesion (clear separation of concerns/responsibilities).

Without loose coupling and high cohesion, your classes will not be composable which is the whole point of OOP...

These characteristics ensure that state is fully encapsulated and does not leak between multiple components (that's when it gets ugly).


Ah yes, the classic "No True Scotsman" OOP defense.


I've provided a very specific definition of what good OOP requires so it's not fair to suggest that my argument is pointing to some vague characteristics or is evasive.

I can look at any project's code and assign it a score in terms of cohesion and coupling of the classes/modules/components. Other people who are experienced with OOP can look at the same code and they will come up with a similar score.


The point of "No True Scotsman" is that you have your own definition of high quality OOP, which is not universal. Maybe yours is the right way of doing things, IDK. But I think others would probably prefer different definitions. For a lot of people, this variation in opinion of how OOP should be used can lead toward a conclusion that OOP in and of itself is a confusing concept and difficult to get "right".

Some people, myself included, just try to avoid this complication altogether, by separating data from logic.


FP solves some issues but simultaneously introduces a new set of issues which creates new 'No true Scotsman' debates around ways to address those new issues... It's like the joke that there were too many competing standards and so somebody decided to invent a new standard to make all the other standards redundant... The net result of this is that we end up with n + 1 competing standards.

IMO, the biggest problem I often see with FP code bases is poor separations of concerns which leads to spaghetti code which is hard to read and maintain. When some state is not co-located the logic which is supposed to be operating on it, you're already throwing high cohesion out the window... And when you do that, it makes is harder to separate the responsibilities of different components because there is no clear ownership relationship between the logic and various bits of state... With FP state can end up being mutated all over the place and it's hard to know who did what.


Every complaint I’ve ever seen about OOP (and other tools) amounts to “we misused and abused the tool/language/whatever so therefore the tool itself is bad “


But don't worry, someone will accuse of the No True Scotsman fallacy shortly.


Article like this miss the question of what purposes OOP serves in practice and so fail to offer alternatives to that purpose.

In practice, OOP basically exists to take some horribly messy program, put an interface around it, and make it slightly less terrible to deal with. Similarly, it also involves creating an interface to a thing a programmer barely understands and letting the programmer do some things with it. Which is to say is about letting the programmer be wrong and only fail moderately - as opposed to making the programmer is right.

Inheritance, one of the worst feature of OOP, has wormed it's way because it's also incredibly convenient.

Everything that OOP is compared to (notably functional programming), is based on a "green fields" paradigm where everything can be controlled. And maybe those approaches work great (though you can't exhibit stuff with they replaced in OO in the trenches and maybe things great).

It's got the tone of "this suspension bridge is far superior to your roll of duct tape" - yeah, maybe you're right but that won't make the duct tape go away even slightly.

I wish someone would come up with a good and immediately applicable alternative to OOP but I can't this sort of critique leading there.


> In practice, OOP basically exists to take some horribly messy program, put an interface around it, and make it slightly less terrible to deal with. Similarly, it also involves creating an interface to a thing a programmer barely understands and letting the programmer do some things with it. Which is to say is about letting the programmer be wrong and only fail moderately - as opposed to making the programmer is right.

I don't think any of this is unique to OOP; rather, this applies generally to the concept of abstraction.


I was referring thing like using member functions to do bounds checking and to make parameters consistent.

That's not really "the right way" to do programming or something generic abstraction gets you. The calling function should just know what it's doing. The Dijkstra quote "Object oriented programs are offered as alternatives to correct ones..." is correct. If you set a wrong parameter, it's ignored and your program works, it might have a more subtle bug from what you thought your wrong parameter would accomplish.

But this is still useful for programs produced by large teams where some people knowledge is limited. It's a mess but no one has put forward an alternative to the mess.


I think Dijkstra's joke is about how object oriented programs tend to eschew correctness rather easily. But I don't think he was saying that's a valid approach to solving problems.

Adding new methods to an object to tame complexity seems paradoxical to me. When something becomes too large, I generally prefer to investigate other means of abstraction/extraction/organization available in a given language.


> In practice, OOP basically exists to take some horribly messy program, put an interface around it, and make it slightly less terrible to deal with.

This is the sort of hype that has followed OOP around everywhere though. Well that's great, of course we would want to take a horribly messy program and make it less messy! Who wouldn't?

But where is the evidence that it actually performs as advertised? Does it make software easier to deal with? Is it superior to alternative ways to achieve that?


But where is the evidence that it actually performs as advertised? Does it make software easier to deal with? Is it superior to alternative ways to achieve that?

What are those alternative ways to make a huge system manageable you allude to? As I said, none of the paradigms put forward as alternatives even claim to operate like this. It's serious question, what are the alternatives in this kind of situation?

And I'll admit, a "horrible messy situation" that we'd look at today is going to be an earlier OOP system.


I'm not making the claim though. I want to know what the data is for your assertion that OOP "makes a huge system manageable" in a way that other approaches or language features can not.

Mind you there are countless millions of lines of COBOL, C, etc out there in production so existence alone does not provide any evidence one way or another.


I wholly agree with this. The problem with OOP is that software are tools that are about processes and workflows. They do things. Processes and workflows are notoriously difficult to model using OOP methods (one reason why design patterns were invented) but actually quite easy to model using imperative or functional programming. I use this software to listen to songs in mp3 format. It plays songs through my computer's audio subsystem. Trying to model this using classes and objects that interact with each other - one for the player, one for songs, one for user input events like pressing forward and pause, one for the audio subsystem, one for the playlists, one for timing events, etc - will not make the software less complex.

My view is that software are like fractals. That is, self-similar and there is no difference between the micro and macro scales. What is right on the micro scale is right on the macro scale and vice versa. What is wrong on the macro scale is wrong on the micro scale and vice versa. Is it "wrong" to implement sorting algorithms using OOP? Yes. Then it is also "wrong" to implement music players using OOP.


> That is, self-similar and there is no difference between the micro and macro scales.

That's quite an interesting way of framing it, I've never considered whether or not software has a "you shouldn't mix your micro- and macroeconomics" problem or not.

Is your conclusion that they are the same at scale based on gut feeling/experience (which I don't mean in a dismissive way, experience is a valuable source of insight) or do you have some concrete examples to elaborate why you think that?


It's just my gut feeling based on experience. I have never seen software problems that are fundamentally different on the macro scale from the micro scale. I have never met anyone who was great at debugging segfaults in C code that was not also an amazing architect. I have never met a great architect that wasn't also amazing at debugging segfaults. To me software development is like math and there is no material difference between "high-level" math and "low-level" math.


Data Oriented Programming. In short, don't entangle your code with data, and the data should immutable.

https://blog.klipse.tech/databook/2020/09/25/data-book-chap0...


Data Oriented Programming originated from video game development and is all about knowing your hardware and making sure you transform data A into data B in the most efficient way possible.

This tends to have other benefits besides performance as the simplification of code that results from focusing on the right thing also tends to help with architecture (until you go ham on optimization).

What it most certainly is not about is immutable data since that's counter to efficiency. It's hard to beat a big fat buffer you poke at directly. Not entangling code with data and focusing on immutable data is Functional Programming, pretty much its definition, not Data Oriented Programming.


A program with only immutable data cannot do anything interesting, almost by definition. Some data has to be mutable.


Only has to be at the root.


Please allow me to decide how I structure my code. I do not need help.


Inheritance in OOP is what I like to think of as "structured patching".

When you create a .patch-file with `git diff`, you are essentially creating an inheritance hierarchy. Imagine having 5 patch files that are applied to the same code file in sequence. This is analogous to an inheritance hierarchy with 5 levels.

patches/inheritance is a very useful tool when you want to make some changes to third party code you depend on before using it - while minimizing the maintenance burden of these changes.

Inheritance/patches is also a very useful tool for describing/representing changes to your own code, but only as a temporary measure - you want to flatten out the levels of inheritance/patching for readability. With .patch-files, this can be done automatically. With OOP/inheritance, this is more of a manual (although still straight forward) process.

A git repository with 1000 commits is essentially an inheritance hierarchy with 1000 levels. Note that git will automatically flatten out these patches when you check out a certain commit - which is what makes git such a great/usable tool. Imagine if git could not do this flattening automatically. Code bases would quickly become unmaintainable if they wanted to retain their change history.


Haskell gives me headaches. I write a lot of Rust, which has elements of both. I think their approach is pretty good, though sometimes regular inheritance would just be easier.

I don't hate OOP. I don't love it either. I tend to write my code in a mix of procedural and diet-OOP paradigms. It tends to produce less state, less inter-dependencies, and doesn't require abandoning stuff like mutable state.

Why do I want mutable state so bad? Because it's the easiest, least abstracted solution to the problem much of the time, and closer to what the compiler will actually want to generate.

I think trying hard to stick to a single paradigm is folly. I like mixing and matching according to the task I'm attempting to accomplish, and I have a firm conviction that my code benefits from this ideology. Encapsulation I do value, but not as a "hard no". I use access specifiers to tell you "you probably don't want to mess with the guts of this thing, it can take care of itself better than you can."

As for inheritance, I don't use it very often. I like how C++ has no base "Object" class. A lot of the time I'm basically writing structs with methods, and I think that's often the right approach.


>diet-OOP paradigms

Ha nice, I love the idea of calling it diet-OOP. "Diet-OOP, same great productivity with 99% less inheritance!"


So are there any resources for simpler way of writing code but for bigger projects (specifically more complexity than toy programs or scripts) or do I only have well written codebases in a similar style as a target to study?

I'm aware that you can write good OO code, that inheritance can be useful (e.g. in shallow hierarchies), etc. but I'm interested in exploring this other side of programming to improve my code


Take any Object-Oriented language. Then modify its definition so that any class can have just a single method (as opposed to any number of methods).

You now have a new language which you could call a procedural- or functional language based on the set of features that were present in the language you started from. But it is no longer an Object-Oriented language.

To make it more concrete start with Java compiler but modify it so it only allows max one method per class.

You now have NOOP-Java. (Non Object-Oriented Java).

Are you happy? Is this NOOP-Java somehow better than the plain old Java? If not really then removing the OOP-ness did not really help did it?. OOP is good. You want to keep OOP, not remove it.

OOP lets you have multiple functions associated with a data-structure the details of which you hide behind those functions (a.k.a. methods). I think THAT is the essence of OOP. You can have multiple functions attached to the same data-structure and only those specific functions can read and write that data-structure.

Isn't that something that really is so useful that getting rid of that feature, getting rid of "OOPness", would be crazy?


Put shackles on your feet then try to run. Now, cut off your feet completely and try to run again. See? Shackles were good for you.


I was able to hold a job at a big company with a large C++ codebase, in the millions of line of code, for 2 months. I was amazed how people there were able to deal with the amount of BS that this code was. I honestly don't have the motivation to thrive with such work, it's deeply non-sensical.

It really struck the confidence I had in C++. The code structure had so much technical debt and non-sense, that most developers never really dare say how awful it was. I was impressed by the blind humility of all of this, it seemed like a very elaborate game of hot-potato-passing and it-s-not-my-fault. Of course this software team was thriving, because it was in a monopolistic domain that would always rain money to write this software, even if it was bad. The manager was a highly motivated guy, very humble, but he also seemed a bit delusional, because fixing this software was going to take at least 5 or 10 more years.

OOP sounds like an utopian abstraction, some kind of "smartest guy in the room" stuff. It's like pure mathematics trying to do practical physics and engineering. It just doesn't work.

It's really time academics start to highlight the dangers of abstraction, and start writing some philosophical approach to software engineering and how to solve problem IN PRACTICE.

Personally, I mostly use namespaces to "encapsulate" data, behavior and code. I just write functions and use data oriented programming, even in python. It's funny how people finds this inadequate and bad practice. I never share code because I'm afraid to go in an argument because of it.

Code that use classes, inheritance, private/protected access, is just impossible to read and follow. It's just obfuscation to me. Unless you're writing a library that is used by many, meaning it requires well-structured inheritance, 99% of developers should not write OOP.

I will never forget this hackernews comment mocking people who were presenting their project, praising how complex it was. OOP seems like it's the main method used by "hostage takers" to make their code only readable to them.

In any kind of engineering, simplicity MUST PURSUED AT ALL COST. Only use complexity as a last resort, and FIRE developers who are unknowingly becoming hostage takers, and who keep innocently arguing that if they can understand their code, anyone can. Advocate of OOP will cost a lot of pain to future developers.


That's because what people call OOP isn't OOP.

> “I invented the term object oriented, and I can tell you that C++ wasn't what I had in mind.” —Alan Kay.

Read here for more info: http://xahlee.info/comp/Alan_Kay_on_object_oriented_programi...


What people call OOP is OOP, literally by definition. Alan Kay may have coined the term, but his version of OOP has nothing to do with what is commonly understood today, and is therefore irrelevant.


> What people call OOP is OOP

That's silly and not how definitions work.

definition noun def· i· ni· tion | \ ˌde-fə-ˈni-shən

an explanation of the meaning of a word, phrase, etc. : a statement that defines a word, phrase, etc.

In other words: words or phrases have actual meanings, regardless of what people call them.

To give you an example, people use "their" instead of "they're" all the time. It doesn't mean that "their" means "they're".


> To see where the problem is, just think about this: If int/float were separate modules that do not depend on each other, where the (int,float) and (float,int) pairs should be defined? Nowhere? Somewhere? In either one of the modules but why?

I'm confused by this argument against multiple dispatch. Can't there be a third module that references both?


Of course there can. The objection makes little sense.


There can be, though what would you call this 3rd module?


So I only do data engineering, backend APIs, Data Science and Machine Learning. And so many times i learned something about OOP, i go excited I implemented it and i ended up with a solution that was really complex and I re-implemented it using functions and procedural code.

However, i do love a library like pandas with their data frames and series etc. which are two really cool and powerful abstractions / classes.

I also noticed this trend in Python/ Data Developers (including myself),that they start writing mostly procedural code and at some point they feel like they "have to" move to classes to be more sophisticated.

Maybe I am also just bad at finding good and easy to understand abstractions that you can re-use like a data frame. But maybe when it comes to data this is just quite hard? Would love to hear about other data-peoples perspectives about OOP.

Also the thing with data engineering the state lives in a DB or a file and not really in my class (because its soo large).


I don't quite understand what this is trying to say. It's a summary review of some reviews? Or something. It's not even clear if the author of this post agrees or disagrees with the claim in the YC News title. It's peppered with sentences like:

"That you don't understand something doesn't mean it's flawed or bad."

Precisely. Many of the arguments against OO are from academics that don't write real-world, large-scale software.

More arguments are from beginner programmers with a mere 2-5 years of experience that have never personally encountered the mess that programming at scale was before OO made it manageable.

Even more arguments are from people that assume that "precisely what language X happens to do" is exactly what OO is or isn't, and then argues from that straw man position.

Those same people then cheerfully embrace microservice or K8s, even though they are object-oriented! In case this is not clear, let me define OO for you:

"Object oriented design is all about encapsulated private state with abstract interfaces that have varying implementations that clients don't need to know the specifics of -- even the type names -- ahead of time."

Sounds like microservices? Or K8s? Or REST? Or service bus? Or event streams? Or any large-scale programming pattern? Yes. That's because abstract interfaces and decoupled components are critical to enabling large teams to collaborate on software too big to hold in the head of any one person.

OO is not intended to solve manager-human-employee-customer categorisations of real-world entities. That's a stupid straw-man argument born of toy textbook examples that aren't representative of real-world programs to begin with.

OO is intended to allow programming at the scale where simple procedural programming no longer works. I didn't understand the need for OO either until I worked at that scale myself. It seemed overcomplicated and unnecessary.

This is why it cracks me up to see language designers (and random Internet bloggers) talk about how "they don't need OO". Yes... you. Singular. One person doesn't ever need OO. Many people do. You're not many people!

Has anyone seen large scale development done successfully in an an "anti-OO" language like Rust? I haven't. The single largest codebase is probably Mozilla's Servo, which is still a toy compared to what's been written in C++ or Java! It was written by a few people, mostly in one building. Notably development of it slowed down and was abandoned. It was never shipped as intended.

How would you even envision a Rust project working with, say, 1000 developers? What happens when there's some new functionality added? Does developer #587 have to go notify the other 999 developers to update their switch statements, pattern matching code, etc... ?

Right now... generally speaking, yes. You'd have to go tell other people to go update their code. As you can imagine, 1000 developers telling 1000 developers to update things is a 1M-level organisational scaling problem.

That's why automated tooling was invented to handle this problem automatically, even at runtime, let alone compile time.

If you think automation is bad, then we no longer agree on the most basic concepts and this conversation is over.

If you have a better method for automating this problem, then I'd love to hear about it! Just make sure it's not OO with a different name...


I need to counter the argument that you can not write complex systems without OOP.

SAPs ERP system is arguably one of the biggest, if not the biggest software system on the planet. It is written in ABAP and ABAP is a procedural/imperative language. SAP is open source and the code is fairly easy to read. Reusability of functions an procedures provided is fairly easy and groundwork for every developer working with SAP to modifiy, enhance functionality or writing add ons.

Or more specifically it was. ABAP got OOP extensions about 10 years ago and since then the code became more and more inflexible and less maintainable for people other than the original authors.

But maybe the main point about ABAP is that despite its procedural nature it allows data centric programming. You take a line of data, transform this line and return the transformed line. No need for abstractions, inheritances and complex design patterns, concerns with mutability etc. Just plain and simple solving domain problems instead of problems caused from abstraction.


> Those same people then cheerfully embrace microservice or K8s, even though they are object-oriented! In case this is not clear, let me define OO for you:

> "Object oriented design is all about encapsulated private state with abstract interfaces that have varying implementations that clients don't need to know the specifics of -- even the type names -- ahead of time."

"OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things." - Alan Kay

You're only using 1/3 there. First-class messages were more important than encapsulation to him.


First-class messaging implies "encapsulation". A message can not directly modify its target. Its target must INTERPRET the message somehow and then decide what to do about it. A message can not modify its target. Only the recipient who receives the message can access its own data, which means its data is "encapsulated".

It is as if you sent me a letter by "first class mail". Great I can read your letter. But your letter can not directly alter the arrangement of furniture in my house. Only I can do that. And perhaps when I read your letter I decide to take such action. Or maybe I decide not to.


> Has anyone seen large scale development done successfully in an an "anti-OO" language like Rust?

Taking "anti-OO" to mean languages that specifically don't have OO capabilities (rather than languages where you can choose not to use OO). Any of the large C code bases?


Very often in code like that you see "struct" types filled with function pointers.. Those are literal OO "vtables", just implemented manually instead of being generated by the compiler.

The Linux kernel is huge, yes, and is written in a "pre-OO" language. It's also full of OO paradigms.

For example, the core kernel developers don't want to have to write huge "switch" statements to handle the thousands of different drivers written by tens of thousands of third-party developers.

What do you expect to see at this API boundary? Perhaps.. and OO API with vtables and everything?

Yup: https://www.kernel.org/doc/html/v4.11/driver-api/infrastruct...

Function pointers in structs as far as the eye can see...


> Very often in code like that you see "struct" types filled with function pointers.

Fair point. Though with the explicitness that's required to do this in C (you have to pass the actual struct pointer that would normally be the receiver, along with a general lack of inheritance) results in code that's much easier to understand than the patterns that the traditional OO languages have created for themselves. I suppose that's less of a criticism of OO itself and more of a criticism of the people who build languages that emphasize OO, and the general ecosystems that crop up around them.

> written in a "pre-OO" language

I would be remiss if I didn't point out that the "OO languages" go all the way back to Simula67, which predates C. Though at that point OO was still in its "academic" phase the way functional programming is today.


For the record, the Linux kernel is not huge, just large. Just saying that because many people here show Linux as a model of a "huge" software project and yet it absolutely isn't. E.g., project-wide refactors/API changes are still performed multiple times per release and done usually by a single-person team, something which would just be unthinkable in huge software projects.

Agreed with the point, though.


What is your distinction between "huge" and "large"? The Linux kernel is over 30 million SLOC now (though a lot of that is drivers). That's "just" large?


I am mostly talking about concurrent contributors, but yes, even 30 MSLOC is just "large", not "huge". Stories of commercial software projects having to build "overnight" on server farms are common.


Are you claiming Rust is incapable of writing microservices, being run on K8s, using service buses or event streams, etc.?

Why focus on building OOP functionality into the language, when it should be the infrastructure and API frameworks that should be built for OOP?


A1) No.

A2) Because it's about 1,000x to 10,000x more efficient.

An awful lot of the "ills" of modern development practices boil down to the lack of ingrained rules of thumb related to performance. The difference between a local function call -- virtual or not -- and a network call can easily be a factor of a million.

This just isn't in the mental model of most developers. The terms "nanoseconds" or "clocks" are not in their vocabulary.

I grew up and learnt programming in an era where OO was considered extravagantly wasteful because virtual function calls had an extra indirection! Those precious instructions -- and more importantly -- the lost opportunity for inlining or CPU pipelining were considered brutal performance hits.

These days, people throw Python into Docker containers and run them remotely on the network to invoke what amounts to a page of code. They call this "modern".

Then they go on Y Combinator News and complain about how OO is "bad" somehow. Quite a few of these people have probably never written a class hierarchy from scratch themselves.

I literally just spent a day talking to some full-time developers with years of experience, explaining how to implement a simple "storage abstraction" OO hierarchy. You know, you have a base interface or abstract class with a bunch of implementations like "S3BucketStorage", "ZipFileStorage", "LocalFilesStorage", or whatever... and then you have the meta-implementations that combine them, such as "UnionStorage", "CacheStorage", and "RetryStorage", each of which take the abstract interface as input parameters during construction. So you can have local files act as a cache for S3 buckets (with retry) that override a local zip file of static content. Or whatever! Combine implementations to suit your whims.

They looked at me like I had grown a second head that started speaking Greek while the other spoke Latin.

Then they wrote some spaghetti code of functions with hard-coded parameters, checked that garbage in to the repo, and then dutifully sent out an email to management saying "job done".

Is OO bad, or are most developers bad? I suspect the latter...


I mean, if you really care about performance, object-oriented is a poor choice as it doesn't map well onto the GPU.


OO runtime dispatch as implemented by C++ via "vtables" of function pointers doesn't even map well to CPUs! The indirection via a data pointer that can change unpredictably is terrible for pipelined architectures. Similarly, this approach generally prevents inlining, especially across dynamic library boundaries.

However, many languages and even C++ with modern compilers can pull tricks to mitigate this. For example, static analysis can often be used to replace virtual calls with direct ones. Similarly, functions can be inlined in many cases, such as a "leaf" class in a hierarchy calling itself.

Languages with "virtual machine" runtimes such as Java and C# can potentially optimise even dynamic scenarios. Java certainly does in some cases.

I think the ideal OO framework would actually be more like what Rust does with traits. Have static dispatch as the default at runtime, but with the traditional OO model of interfaces, classes, derived classes, etc... Dynamic dispatch should be an option, but not the default. Ideally, dynamic dispatch should be used only on the boundary of binary modules such as DLL files or kernel-to-user-mode ABIs.

Note that OO was also designed to reduce compilation times by decoupling implementation from use. So if developer A updates an implementation (class/struct) of an interface/trait, then developer B using that interface can use incremental compilation without having to recompile the usages of the interface! This saves a lot of time for large code bases.

One reason Rust is notoriously slow to compile is because it always recompiles everything -- both implementation and usage of interfaces.

Again, a hybrid approach could work: dynamic dispatch by default for debug builds to enable efficient workflows, and static dispatch by default for release builds for runtime performance at the cost of longer build times.


Rust is not an "anti-OO" language. It has a basically complete featureset for OO programming with the one exception of implementation inheritance. It is fully usable for programming "in the large".


You say that it is fully usable for programming in the large, but other than Servo I can't think of any large Rust projects of the top of my head.

Certainly there are none with more than 1K developers collaborating.

I'm aware that you can have "dyn Trait" in Rust and a Box of a trait also provides runtime polymorphism. However, it just has too much friction due to explicit "no more OO!" decisions by the Rust core language team to enable truly large-scale programming, at least in my humble opinion.

I think such decisions stem from logic like this:

"I saw OO used extensively at <insert large project> and the <project> was a huge mess and nobody was happy, hence OO is bad and should be avoided."

Meanwhile the logic is more like:

"<large project> needed OO because it was large, and it was unpleasant to work on because it was large, not because it was OO. It would have been worse if it wasn't OO."


You can say this for ANY language. Literally any language that can assign a function to be part of some struct could be "OO"... which is basically any language including C.

So given the pervading reality, colloquially when we say the term "Object Oriented Language," we are not referring lisp or C or haskell even though all of those language are technically OO.

The same can be said for Rust. Rust is NOT OO.


There is some very large scale Rust stuff happening at Amazon; whether it's OO or not I do not know.


>I find it much easier to formally verify things correct than to test that they're correct.

This rise in me the famous joke by Knuth: Beware of bugs in the above code; I have only proved it correct, not tried it.

I wonder which tools he uses to prove correctness, and on which language. I know about Frama-C, Coq, and things like that. But I never had the opportunity to use them out of university. On the same topic, in a recent ACM publication, Ian Joyner compare Frama-C with a lipstick on a Pig.

https://cacm.acm.org/magazines/2021/12/256941-common-ails/fu...


In that case I think Knuth referenced proving it by hand, e.g. with Hoare triples.

You could argue that hand proof is useless, but by forcing yourself to go through it in tiny steps you can actually sort out a lot of oversights.


C is a pig of a language, so tools that show just how horrible it is, how many bugs are unwittingly written, and how hard it is to make its code safe, could also help pushing people towards better languages.

These tools are more like putting on X-ray glasses and seeing how your fast-food nuggets are made. You will still eat them if you have to, or if you're in a hurry, but you'll be more inclined to take the time to learn cooking, so you can eat healthier food later.


Whenever I hear complaints about a popular programming method, I have some real doubts. Modern software gets better every year.

There may be better ways, but what we have works.

I don't know if the modern world would be possible without OOP. If the only language in the world was Haskell, C, and SQL.... would I have actually learned them, or would I have chosen a different career? Would there be enough devs to have as much software as we do now?

It's hard to tell, because almost everything big is written using OOP. Few things on the scale of LibreOffice or Blink are pure functional.

And Java seems to work exceptionally well for large scale projects.


For my part, I've tried to be as overstated as possible, as you can tell from the title "Object Oriented Programming Is An Expensive Disaster Which Must End." Written in 2014, and discussed several times here on Hacker News, it remains the most popular technical essay that I've written:

http://www.smashcompany.com/technology/object-oriented-progr...


every time I see this kind of posts I remember the good old "Qc Na" koan[0]

> The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."

> Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.

> On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.

[0] http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/m...


The rust book has a great section about OOP and i think solves alot of the issues discussed in these articles at length.

http://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/sha...


Pretty tangled read. This seems to be an example of write-only code, but applied to essays. It appears the author is in favor of OOP but it's a little hard to tell.


> On claims of unit testing and OOP, I'm not going to go there much more because I still don't unit test my stuff. I'm currently not against it. I just don't know about it much. I find it much easier to formally verify things correct than to test that they're correct.

Ah, I see, OP has never written nontrivial code.


- phillips screw drivers are obsolete you should never use them just use torx and throw away anything that uses phillips screws because everybody is doing it. There never was any merit to using one vs the other either, just that people who are still using phillips screws need to get with the times and use torx.


Microsoft word is the most sophisticated word processor, people who use nano and vim are just behind the times... you can do anything with microsoft word. you can do anything with microsoft excel for that matter it's turing complete https://www.infoq.com/articles/excel-lambda-turing-complete/

so its realistically the only programming language you'll ever need and it's the right tool for every job because its point and click and it interfaces with the real world so that people can relate to it better and the learning curve isn't as steep.


You know what is even more valuable? Good documentation outside the code, and good comments in the code.


OTOH:

1) Most of the time GUIs use OOP, and its the "other designs" which have a hard time being successful.

2) The rejection of multiple dispatch is very "hand wavy", a lot of Julia's success is linked to its multiple dispatch feature.


Meh. Is this a hot debate still?

No mention of Ruby or smalltalk in this post, which i think of as "true" OO languages, down to the runtime. The ruby object model has its merits! Sandi Metz's POODR is a fantastic intro into OO _and_ a compositional approach to design.

FP vs OO is always a false dichotomy for sure. Actors and messaging appear in FP languages. Inheritance surely has nothing to do with OO. Inheritance means nothing for data, and it's almost a bug to extend Record types.

In short, this post seems to rage against inheritance and blind use of design patterns, not the spirit of OO. But the post also qualifies that "that is what an OO advocate would say".

Consider Typescript. The same program can be written with `class`es, or as a module of "loose" types and functions. Really, lets pick a mix that best represents the problem we're solving? I think OO can be a _useful complement_ to FP and other paradigms.


It is really weird in 2020 we are still seeing those largely ideological essays against OOP.

The solution is simple, there are tons of languages to use today in a single service/application. Just create your own and put it on the market for validation, if OOP or not is really at the center of productivity then it will be addressed.

Otherwise, I really don't understand the point of such rant posts, feels like marketing and personal flexing.


I only played with Small talk in school as it was before my time but it was great. As I am working at a large Ruby shop currently, Ruby is a lovely language. Ruby really does solve many of the issues I had with Java. Its a very elegant language. I think the behavior of any object taking messages is great, though thats not used much in professional code it seems. I think more people should give ruby a shot.

I do wish it were a bit faster ,but hopefully truffle ruby, sorbet, yjit, etc. can put in the work to fix that.


I completely agree with this. I love writing functional Java. The issue is mutation, not OOP


Would you have any resource on functional Java? My experience with 8 and onward is better for sure. I find myself writing more robust filtering / map-reduce logic. But that’s kind of it.

For instance I don’t feel like function are first class citizen still. ( I almost never user higher order function in Java, even if it’s possible )


Effective Java [0] is the single most important book I can recommend for any Java programmer. This article [1] gives an overview of some of the primitives you can use for writing functional Java. Lombok [2] is a very common library that makes writing functional Java much more ergonomic with its `@Value` annotation, but that might not be needed anymore with Java 14's Record types [3].

The book Clean Code [4] helped me a lot to really learn how to write clean Java, and many of these ideas directly translate into writing good functional code. One of the key takeaways was just how small functions should be which incidentally is a great thing to learn when functions are your main unit of composition.

Java isn't a purely functional language, so you obviously will always have some impurity regarding state/mutation. I personally try to do the following: 1. Keep all state in some top-level class and let everything else be immutable 2. Nearly every class I write is immutable 3. Follow common OOP principles like SOLID 4. Write reactive code with heavy use of Optional and Streams

Here's [5] an example repo of board game written using those ideas.

[0] https://www.oreilly.com/library/view/effective-java/97801346... [1] https://www.baeldung.com/java-functional-programming [2] https://projectlombok.org/ [3] https://www.baeldung.com/java-record-keyword [4] https://smile.amazon.com/Clean-Code-Handbook-Software-Crafts... [5] https://github.com/harding-capstone/logic


Thanks for your answer, I think I’m stuck with Java for the time being but I least I now cater to a modern codebase. I think it’s … alright. But I miss the function being truly a first class citizen.

Those advice ring familiar or interesting. I need to give effective Java a second look. It’s been year and I’m a different dev now.


God. Those example how to archive currying or composition are painful to read. Not wonder why I never write code this way in Java. Still. I want to be more diligent in that domain because I know it pays off down the line.


As a professional dev who has made a career out of working in oop languages and codebases, I agree, and it took me far too long to realize that when it comes to oop, the emperor has no clothes.

To this day, oop advocates can't even agree on what oop even is or means.

Apparently oop as envisioned by Alan Kay was supposed to work like cells in the body that pass messages between each other and take actions independently.

Why? Who knows! It was never really explained why literally one of the most complex systems imaginable, one that we still really have very little idea how it even works, should be the model for what could and should probably be a lot simpler.

Today's modern oop languages are probably very far from what Kay envisioned (whatever that was), but it's remains unclear why classes and objects are "better" than the alternatives.

And before anyone goes and comments aksully code organization blabla like yes but code organization can be great or shit in oop or fp or procedural codebases, it has nothing to do with the "paradigm".

Let alone that the entrenched, canonical, idiomatic coding styles of most modern oop languages encourage state, mutability, nulls, exceptions and god knows how many trivially preventable entire classes of errors.

Granted, most have now started to come around and are adopting more fp features and ideas every year, but still.

---

EDIT tacking on other old comments:

---

Don't get me wrong, writing programs like cells in the body that pass messages between each other and take actions independently is an interesting idea which deserves pursuing, if nothing else but to satisfy our curiosity and seeing to what if anything it's applicable and suited. (and even if the answer turns out to be "nothing", we've still learned something!)

But going from there to making strong claims about it being a more or less universally superior paradigm for computing and writing code, with little to zero evidence, that's a huge, huge stretch.

To the degree Erlang and Actors work, I think that's kind of a happy coincidence, and not due to any rigorous work on Alan Kay's part.

---

Just look at all the "OOP" languages where both the language developers and the user community are coming around to the facts that

* immutability and absence of state is preferable to mutation and statefulness

* Option/Maybe types (or "nullable types" which are a shoddy implementation of the same thing) are better than null

* Either/Result types are better than exceptions

* making things implement map, filter etc and sending in a function that describes what you want to do is better than manually eg looping through lists etc

etc etc etc

Anyone who doubts how endorsed this is, just read what Brian Goetz and Josh Bloch have to say about how to code in Java.

Just imagine if these languages had been implemented with these ideas in mind from scratch instead of the current situation of trying to adopt and retrofit this style when the core libraries fundamentally don't support it.

The current trend of "OOP" languages is basically inexorably heading towards FP and abandoning the old school "OOP" style. Eventually they will only be nominally "OOP", mostly in order to please people who have irrational attachments to labels like that, but be way more FP in nature and in all but name.

For what it's worth, people shouldn't be irrationally attached to the "FP" label either. Labels aren't important - what matters is the code, how easy or hard it is to reason about it, how well it avoids entire categories of defects from even being possible etc etc.

https://proandroiddev.com/kotlin-avoids-entire-categories-of...


I agree that both the label OO and FP is overloaded with multiple indiscernable meanings.

I mean Kay's OO, inheritance, class, polymorphism, abstraction, those are mix of paradigm, design pattern, and language feature.

So does FP with its monads, sumtypes, referential transparency

I guess what can make a better discussion is to take apart the label into these individual items and examine them one by one.

Inheritance - bad - get languages to safely discourage its user from using it

Actor and messages - good - get people to understand the concept and how to implement it in each languages

Immutability - good - let's announce how it helps reduce errors

Functional domain modelling - good - let's make everyone know how to do it

Foreign jargon of FP traits - bad - let's make a more walkable learning curve to those jargons

And so on and so on


> Apparently oop as envisioned by Alan Kay was supposed to work like cells in the body that pass messages between each other and take actions independently.

>Why? Who knows! It was never really explained why literally one of the most complex systems imaginable, one that we still really have very little idea how it even works, should be the model for what could and should probably be a lot simpler.

If you're curious, I think it'd help to read. In it he illuminates what he means. http://worrydream.com/EarlyHistoryOfSmalltalk/


Awful writing. Rambling and incoherent. I do not know what the author was trying to say.

> I just recently figured out myself the pieces I was missing to writing effortlessly skimmable texts.

No; no, you didn't.


-- (Even if you don't read this whole comment, I urge you to watch this fantastic video which illustrates procedural, OOP(lite), and FP approaches to a sample problem: https://www.youtube.com/watch?v=vK1DazRK_a0 (Despite the name, it illustrates JavaScript, not Clojure). If you follow this approach, you don't even have to tell people you are doing FP. You'll just end up with better, more understandable, and more easily testable code.) --

The early (early as in C++ days) benefit of OOP was probably that it made programmers stop and plan before coding.

Having learned FP 20 years after learning OOP, I feel certain that I can do the same things in less and more understandable (and MUCH more easily testable) code in FP.

OOP was awesome for a small set of cases and for academic scenarios. But like REST, it doesn't map well to all needs. Then it becomes awkward and unnecessarily complicated.

FP, or simplified as immutable data transformations with necessary mutations pushed to the edges, works everywhere, all the time. In FP you can still choose to model your data in hierarchies, keeping some of the useful bits of OOP. But it's still data in, results out.

Until you've spent time on real projects in both, you cannot appreciate why FP is superior.

(And to be clear, I'm not even talking about Haskell and type-obsessed FP. Perhaps because I'm not a Haskell guy, I can't appreciate it. But it smells to me like an extreme of a good thing (and therefore not usually the best thing for the situation); but I digress.)

Concretely, I worked recently for a client that needed Ruby on Rails development done on a production product. It was fairly OOPish (in the Ruby way, which is to say much less insane than Java from the past OOP). Even so, the OOP at the business logic level was unnecessary. Testing was complicated and full of code to mock and fake.

When we were given the task to build a completely new solution to the same problem, greenfield, I immediately began building modules with almost entirely pure functions. Because Ruby isn't designed for this approach, it does involve a bit of care to respect memory and object copy time costs. But in many business cases, the volume of data being processed isn't significant or isn't fast-repetative.

The new product had many fewer lines of code, and the test cases were as close to beautiful as maybe is possible for tests. That's a different subject for debate...

The less experienced devs took a bit of time to accept and adjust, but the mid-level engineers became big fans of the approach. The juniors just accepted it (so nice :D). Test coverage went up, and the cost of adding new features went way down.

My FP languages of choice are Clojure and Elixir in that order. Elixir does offer some pretty great features that Clojure doesn't have (extensive pattern matching), but the syntax is imo very noisy compared to the utter simplicity of s-expressions. Either is fine for me. Ruby is fine, with some care. Python too. Vanilla Javascript can be fine, and some libraries can improve this.

Anyone who has read this far and is not convinced, I urge you to follow some Elixir tutorials and reach the point of grokking it. Then if you're a web dev, Phoenix is a fantastic framework. Also, the Erlang (BEAM) VM provides so many useful structures and utilities to allow you to build big distributed things easily compared to other languages.

Be warned though: once you do this, you will forever be frustrated by OOP codebases.


hah, is this the return of "topmind"? I miss that guy, he was the anti-OO fanatic of the 2000s on usenet and c2.com.


What's the pattern for implementing a cache without state? How about a command buffer? Not trolling, seriously curious.


> If I were smarter than I am, I might have went deeper on this regard.

Can't tell if this is deliberately ironic...


I never understand all the hate for certain tools. OOP is a tool, FP is a tool, and other programming paradigms are different tools. If you're bad a software architecture and you tend to write convoluted OOP software, you're probably also going to make a mess using FP as well. Applied correctly, they can both be great tools for different problem sets.

Writing something that inherently has a lot of state, like a simulation, game, or user interface? OOP might be a good choice. Working on a backend API that's basically a database wrapper, a data processing pipeline, or something else with less inherent state, FP may be a better tool. Choose the rights tools, design good architectures, and stop blaming the language paradigm.


I do feel OO is the default for almost all companies and that requires a lot of debate to start to change so you can use other tools though. I remember working for a C# shop where I had to do some pretty simple data ingestion and cleaning from the NHS API that would have been fantastic in F# with it’s composition patterns, pattern matching, and type system. However because it wasn’t OO I couldn’t get support and so I had to write a lot of factories instead.


I generally feel the same, although I can say just as a pragmatic issue, the biggest problems I have ever faced in programming by far have been in dealing with poorly structured inheritance systems and object hierarchies in other peoples' libraries. Bugs are inevitable but problems with object hierarchies seem to screw up everything unnecessarily in a way that doesn't happen in less object-oriented functional and procedural languages. Sometimes it feels like this extra layer of ways to shoot yourself in the foot, and the feet of everyone around you.

Strong typing can be a blessing and a curse, and screwing up type signatures through poor foresight can be a problem regardless. But get into elaborate object systems and it can be a different level. When they're done well it's no problem, but when they're done poorly, it's maddening.


Correct. Complains against OOP is like complaining a hammer not suitable for a task which you should have picked up a wrench in the first place.


The complaints against OOP would imply an analogy that OOP is a bad hammer when you do need a hammer.


Tl;dr: [nothing, really.]

"(Anything)-oriented programming" is obviously stupid: the world doesn't conform to your (anything), whichever you choose. QED, an (anything)-oriented programming language will be the wrong thing most of the time.

A general-purpose programming language needs to provide support for solving actual problems, whatever they are. Any program big enough to be interesting addresses sub-problems of different kinds, that each need their own treatment. Most problems benefit from a mix of approaches.

Once in a while objects are exactly the right thing. Then, if your language has stuff for that, lucky you; otherwise you need to cobble something together. Likewise, when any other formalism matches.

Almost always when people complain about OOP, it is because they want their (other-thing)-oriented language to get respect. That does not end well: the same arguments against OOP apply equally well to their thing, given trivial adjustments.

So, nothing to see here.


people who hate "OOP" are usually ones that fail to understand how to use some language features. prime example is "Circle–ellipse problem" which demonstrates how people fail to realize inheritance is about code reuse and not about representing hierarchies of abstract ideas (and i wonder where they got that wrong info from, perhaps they extrapolated it from the name "inheritance"?).

same goes for all the other language features.


The author of this article thinks that OOP basically means adding vtable pointers to the heads of structures, and shallowly dismisses multiple dispatch.


The author has a point. Yet, most mainstream languages provide OO features; especially many recent ones. Even languages like Rust and Go have some limited OO features. And many languages also add features common to functional programming languages. So, I guess the consensus among language designers is that it's not all bad.

I would say early OO is very different from what is practiced today. I use Kotlin mostly. It's obviously an OO language but it puts some interesting twists on it relative to earlier languages (like Java):

- classes are closed by default and you must define them as open to be even able to create a subclass. When you do, you must explicitly label things in classes that you override. This prevents, un-intential abuse of inheritance that is common in many Java frameworks. E.g. Spring has riduculously deep inheritance hierarchies. When I was still using Java I had a simple rule: any form of class extension is probably something I need to get rid off. Delegation is just preferable in my opinion. I almost always end up regretting class extension to the point where I rarely consider using it.

- Speaking of delegation, the Kotlin language designers obviously agree with this and added interface and property delegation to the language. This is just syntactic sugar but it's awesome. I can take any class and pass myMap: Map<Foo,Bar> into the constructor and then add implements Map<Foo,Bar> by myMap. And just like that you have extended a class but without actually extending it. I can even override some of the methods (because it implements the interface). They basically provided syntactic sugar for a common design pattern: delegation, which you should almost always favor over inheritance IMHO. Property delegation is equally powerful and you can use it to e.g. lazily initialize a property foo: String by lazy { someFunctionThatReturnsAString() }

- it encourages the use of val variables that cannot be reassigned. If you want that, you need to use var. If you define a var and don't reassign it, the compiler will warn you to use a val instead. Immutability by default is encouraged and it helps with e.g. asynchronous code and a few other things.

- it has sealed classes (and interfaces) as of a few versions ago. The advantage of those is that the hierarchy is closed after compilation. So you can't add more sub classes and this benefits the compiler doing some optimizations. Likewise, value classes are now a thing. In the rare cases I do use inheritance, I use sealed classes.

- it actually discourages class extension in favor of using extension functions and properties. This is much cleaner and does not suffer from a lot of the problems associated with inheritance. For example, you can't actually override anything this way. Extension functions are surprisingly useful and I use them a lot. They even work on type aliases or on nullable types. So you can call a function on a null value for e.g. a nullable generic type T and it's not going to trigger a null pointer exception when you do that. More languages should add this. This just removes a lot of use cases where you might have used inheritance or interfaces in the past. This also removes a lot of the need for multiple inheritance; which is problematic in the few languages that still support that.

- having default values on parameters in functions means that you rarely have more than 1 constructor for classes. You can add more constructors, but it's just not something you'd need often and certainly not to support different combinations of properties. Constructors have no body either. All that happens is assigning properties. This enforces the sane rule that constructors must do no work. If you need to do work on class creation, you add an init function.

I'm sure some language designers have plenty of nits to pick with Kotlin. But for me it's a very pragmatic language that mostly manages to nudge people to do the right things while providing them with a lot of convenience. Scala people tend to look down on it for example and that language does have some interesting features. But then I find most Scala code to be utterly unreadable because. Purity has a price, I guess. And of course many Scala coders consider its OO legacy to be somewhat of a mistake apparently.


Should be marked (2020).


OOP hate is evergreen


Added. Thanks!


Isn't OOP mostly just a buzzword?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: