I still think OO provides a pretty easy mental framework for programming. You can get good results. Bit of discipline without going crazy and it works really effectively. Despite its shortcomings.
I still think OO provides a pretty easy mental framework for programming. You can get good results. Bit of discipline without going crazy and it works really effectively. Despite its shortcomings.
It seems to me that part of the problem is that OO doesn't force you to have discipline and/or without constant vigilance (which product owners are never willing to schedule for) the system inevitably gets out of control over time.
On the other hand, it seems to me that the core principles of functional programming (immutability, functional purity, construction via composition, and declarative programming) serve as a check that prevents things from getting out of control.
That being said, I think it's worth considering that all of the "core principles" of FP that I mentioned could be incorporated into OO. It also seems like FP can be more prone to out of control syntax (e.g. unwise use of point free style).
The real issue: discipline has to be learned, often by experience, it isn’t forced. If you force it, then no one knows why things are they way they are.
It's more accurate to say "if you don't teach why' then no one knows why". Forcing or not has nothing to do with it.
I would argue "forcing" is strictly better, since learning discipline requires experience in doing things in every other wrong way. While that's great for learning on your own, I wouldn't want developers to "learn discipline" like this in production.
Imperative programming languages (like C and JavaScript) don't generally impose any discipline on the users.
While JS supports it in many ways, it's still not a style inherent in a multi-paradigm language like Javascript nor is it (really) the primary style in popular frameworks - despite the fact inspiration from FP languages/libraries has been increasingly common in popular frontend frameworks.
Additionally, even if you go full-bore FP on JS, it's still not the same. Almost no one goes full-bore FP in JS because it really doesn't make sense to nor is it an easy thing to do.
(And I say all of this as a frequent user of https://ramdajs.com)
Also to me, FP has an emphasis on avoiding mutating state. The second example on the React front page shows how to mutate state, and then they just build from there. I don't use React, but looking at those examples, it all looks very OO to me.
Hooks are closer to functional programming, though (but were released only a couple months ago)
I think this view disrespects history a little. C was one of the earliest languages to be conceived upon a foundation of structured programming principles, i.e. block structure, sequence/selection/repetition, subroutining. (Okay the language still has goto, hopefully we can agree to not make a big deal out of that.) The kind of discipline proposed by structured programming was far from universally well-received at the time, and I think it's fair to say that it lead to huge improvements in the quality of codebases everywhere, and is one of the great successes of programming language thinking of the '70s.
C is also statically typed. It's obviously easy to blow all sorts of holes in C's type system, but if you'd go so far as to say that C's types impose no discipline at all, I'd ask you to try teaching C to a room full of compsci students who have been raised on something like Python.
I love C, but I'm hard pressed to think of any language other than assembly which is more willing to get out your way if you so much as nudge in an undisciplined direction. C certainly does not impose much discipline on programmers, but it allows them to bring some if they walk the line.
> Okay the language still has goto, hopefully we can agree to not make a big deal out of that.
> It's obviously easy to blow all sorts of holes in C's type system
Yes, I'm willing to politely ignore evidence which refutes your point. I mean you were kind enough to make my case for me. :-)
> I'd ask you to try teaching C to a room full of compsci students who have been raised on something like Python.
Dynamic vs static typing seems orthogonal to what we're talking about here, but maybe I'd have to think about that more. Python just waits to catch your type errors until runtime. Comparing that to JavaScript in a browser which silently ignores your errors (or happily performs crazy type conversions), Python seems disciplined in comparison.
If I could go back in time and unilaterally make one change to Java, it would be to make `final` default. But if you just get in the habit of using it (the IDE helps), non-final variables look broken. And once objects have all-final fields, immutability just starts spreading upward in your code.
final List x = new ArrayList()
stops x being reassigned but does nothing to stop the downstream code from mutating the contents of the list.Never expose your collections directly (unless you are willing to go copy on write and immutable objects inside the collections, the latter is very welcome, though)
It seems to be what we do, I’d say fp is in the same place. My CS program was heavily built around the ML family of languages, specifically Standard ML, with the algebraic types, functions, pattern matching (on your types,) etc. it seems like that “functional programming” is a radically different thing than what people do in js or erlang and call it that. It all comes around, I guess, static types were pretty gauche 10-15 years back and now how many folks are using typescript to make their js better?
With respect to your setters and getters question, particularly in regards to Python... The @Property feature in Python is just a specific implementation of the setters/getters OO design principle. I can easily be convinced typing foo.x is better than foo.getX(), but I have a hard time having a strong emotional reaction to one vs the other if the language allows them to have the same benefits.
When you want a value from a module/object/function/whatever, whether or not it's fetched from a location in memory is an implementation detail. Java and co provide a short syntax for exposing that implementation detail. Python doesn't: o.x does not necessarily mean accessing an x slot, and you aren't locking yourself into any implementation by exposing that interface as the way to get that value. It's more complicated than Java or whatever, here, but it hides that complexity behind a nice syntax that encourages you to do the right thing.
Some languages provide short syntax for something you shouldn't do and make you write things by hand that could be easily generated in the common case. Reducing coupling is still a good idea.
Most people don't have a problem with getters and setters, they have a problem with writing pure boilerplate by hand. Languages like Python and Lisp save you from the boilerplate and don't provide a nicer syntax for the implementation-exposing way, so people don't generally complain about getters and setters in those languages, only in Java and C++ and things.
I specifically object to the useless abstraction, not the boilerplate (boilerplate is cheap).
I don't view it as a useless abstraction, because I view it as the natural way of things. I view specifying that your get/set implementation is and always will be implemented as slot access to be uselessly sharing implementation details that does nothing but freeze your current implementation strategy.
I think a better question is when that abstraction gets in your way. When does it bother you that nullary functions aren't reading memory locations? Why do you feel that's an essential thing to specify in your public interface, as a default? There's nothing stopping you from writing code in Python and mentally modelling o.x as slot access, because it follows the interface you want from it.
If you only care because it's something extra you have to do, then that's what I meant by boilerplate. I think it's a misfeature of Java's that it presents a model where that's something extra you have to do.
I understand your point, but I think you misunderstand what "abstraction" means. "abstraction" doesn't mean "function" (although functions are frequently used to build abstractions), and if you have "dynamic properties" (or whatever you'd like to call them) a la Python, then you're still abstracting. My point is that abstracting over property access (regardless of property-vs-function syntax) is not useful, or rather, I'm skeptical that it's useful.
> I think a better question is when that abstraction gets in your way. When does it bother you that nullary functions aren't reading memory locations? Why do you feel that's an essential thing to specify in your public interface, as a default? There's nothing stopping you from writing code in Python and mentally modelling o.x as slot access, because it follows the interface you want from it.
I think this is a good question, because it illustrates a philosophical difference--if I understand your position correctly, you'd prefer to be as abstract as possible until it's problematic; I prefer to be as concrete as possible until abstraction is necessary. There's a lot of mathematical elegance in your position, and when I'm programming for fun I sometimes try to be maximally abstract; however, when I'm building something and _working with people_, experience and conventional wisdom tells me that I should be as concrete and flat-footed as possible (needless abstraction only makes it harder to understand).
To answer your question, that abstraction gets in your way all the time. The performance difference between a memory access (especially a cache-hit) and an HTTP request is several orders of magnitude. If you're doing that property access in a tight loop, you're wasting time on human-perceivable timescales. While you can "just be aware that any given property access could incur a network call", that really sucks for developers, and I see them miss this all the time (I work in a Python shop). We moved away from this kind of "smart object" pattern in our latest product, and I think everyone would agree that our code is much cleaner as a result (obviously this is subjective).
TL;DR: It's useful to have semantics for "this is a memory access", but that's unrelated to my original point :)
getArea() {
return this.width * this.height;
}
getIcon() {
// if icon hasn't been loaded, load it
return this.icon;
}Using an array without having to (manually) calculate the size of the objects contained within is like the major triumph of OO. This is a getter that you almost certainly use constantly.
Please try to consider your statements and potential counter factuals before spraying nonsense into the void
I've used arrays in countless OO and non-OO programming languages, and I do not recall ever having to manually calculate the size of objects contained therein – what are you talking about? Only C requires crap like that, but precisely because it doesn't have first class arrays.
size_t len_a = sizeof(a)/sizeof(a[0]);
or my_pseudo_foo_array = (foo*) malloc(len * sizeof(foo));My claim was that getter abstractions as described by the GP (abstracting over the “accessed from memory” implementation detail) are not useful. Why do you imagine that your array length example is a reasonable rebuttal?
Sorry for the way I communicated- I was tired and should have reconsidered.
No worries, it happens. :)
> Its not the length of the array. Its using things like array[20]. Yes that exists pre-OO and outside of OO, but its the foundational aspect of OO and one of the strongest use cases.
I'm not sure what you're getting at then. Indexing into an array? Are you making a more general point than arrays? I'm not following at all, I'm afraid.
You aren't responsible for maintaining any of the internal details, it just works like you want it to. My example was with the getter for the item at index 21 (since you had specifically called out useless getters), but equally well applies to inserting, deleting, capacity changes, etc.
I think I see what you mean, although I think it's worth being precise here--arrays can be operated on via functions/methods. This isn't special to OO; you can do the same in C (the reason it's tedious in C is that it lacks generics, not because it lacks some OO feature) or Go or Rust or lisp.
These functions aren't even abstractions, but rather they're concrete implementations; however, they can implement abstractions as evidenced by Java's `ArrayList<T> implements List<T>`.
And to the extent that an abstract container item access is a "getter", you're right that it's a useful abstraction; however, I don't think that's what most people think of when they think of "getter" and it falls outside the intended scope of my original claim.
Er, aside from C and ASM, few non-OO languages require that kind of manual effort. That's not a triumph of OO, it's a triumph of using just about any language that has an approach to memory management above the level of assembly.
C# also 'fixes' that. o.x could be a slot or it could be a getter/setter.
fooobj.events += my_eventhandler;
Which isn't surprising, given that Delphi took the idea from Eiffel, which share the same Pascal influence, and was designed by Anders.
That is the important thing sometimes. At other times the important thing is to provide a flexible, fluent public interface that can be used in ways you didn't intend.
It really depends on what you're building and what properties of a codebase are most valuable to you. Encapsulation always comes at a cost. The current swing back towards strong typing and "bondage and discipline" languages tends to forget this in favour of it's benefits.
That scares me. How do you maintain and extend software used in ways you didn't intend?
Quality assurance should be challenging.
It scares you because you're making some assumptions:
1. You assume that I'm writing software that I expect to use for a long period of time.
2. Even if I plan to use my software for an extended period of time, you're assuming that I want future updates from you.
Let me give you an example of my present experience where neither of these things are true. I'm writing some code to create visual effects based on an API provided by a 3rd party. Potentially - once I render the effects (or for interactive applications - once I create a build) my software has done it's job. Even if I want to archive the code for future reuse - I can pin it to a specific version of the API. I don't care if future changes cause breakage.
And going even further - if neither of these conditions apply the worst that happens is that I have to update my code. That's a much less onerous outcome than "I couldn't do what I wanted in the first place because the API had the smallest possible surface area".
I'll happily trade future breakage in return for power and flexibility right now.
You get into the same issue with eg making your interface be that you return an array, instead of a higher-level sequence abstraction like "something that responds to #each". By keeping a minimal interface that clearly expresses your intent, you can easily hook into modules specialised on providing functionality around that intent, and get power and flexibility right now in a way that doesn't hamstring you later. Other code can use that broad interface with your minimal implementation. Think about what you actually mean by the code you write, and try to be aware when you write code that says more than that.
I think it's interesting that you associate that interface-conscious viewpoint with bondage and discipline languages. I mostly think of it in terms of Lisp and Python and languages like that where interfaces are mostly conceptual and access control is mostly conventional. If anything, I think stricter type systems let you be more lax with exposing implementations. In a highly dynamic language, you don't have that guard rail protecting you from changing implementations falling out of sync with interfaces they used to provide, so writing good interfaces and being aware of what implementation details you're exposing becomes even more crucial to writing maintainable code, even if you don't have clients you care about breaking.
Of course all this stuff goes out the window if you're planning to ditch the codebase in a week.
1. By declaring a getter without a setter, you could make a field read-only.
2. A setter could trigger other side effects. Specifically, the JavaBeans spec allowed for an arbitrary number of listeners to register callbacks that trigger whenever a value gets changed.
Of course, nobody actually understood or correctly implemented all this, and it all got cargo culted to hell.
A big problem is cargo culting without reading the CS references.
Or you get values from your objects. You need getters for this, but you can guarantee immutability and apply functional programming principles to your code.
You can't have your cake and eat it too. At the end of the day, you need values.
My guess is there's a correlation between overengineering and career success, which drives it. Simple, 'KISS' style code is the easiest to work with, but usually involves ditching less essential libraries and sticking more to standards, which looks crap on your resume. Most interviewers are more interested in whether you can piece together whatever stack they're using rather than whether you can implement a tough bit of logic and leave great documentation for it; so from a career perspective there's zero reason for me to go for a (relatively) simple 100 line solution to a tough problem when I can instead go for a library that solves 100 different use cases and has 10k lines of documentation that future devs have to wade through. The former might be the 'best' solution for maintainability but the latter will make me appear to be a better engineer, especially to non-technical people, of which there are far too many of on the average team.
Recent writings by Joe Armstrong are also resonate with me the same way.
Ruby is far from being my favorite language, but I like how Structs "solve" the getter/setter problem in it:
my_struct = Struct.new(:field_one, :field_two)
It doesn't clutter your code with multiple lines of boilerplate, and it returns a regular class for you to use, not breaking the "everything is a class" constraint.Functional Programming (and Logic Programming) are better than other paradigms because, unlike Java (or C++, or C#...) there is an emphasis on correctness, and the people working on FP compilers (like Haskell and Idris) are utilizing mathematics to do this.
No idea on your opinion on mathematics, but to me Math/Logic reign supreme; the more mathematically-bound your program is, the less likely it is to do something you don't want later.
Compare this to Java. It's 2019, and we're still doing `if (x==null) return null` all over the place (I'm aware that the option type exists but that doesn't really help when it's not enforced and none of my coworkers use it). How about having to create six different files for something that I could have written in 20 lines in Haskell? Or how about the fact that the type system exists to help with optimizations, and due to a lack of support for structured typing, it can only be useful for that.
I realize that I'm picking on Java, but Java is the biggest target when it comes with OOP as the industry understands it. I personally cannot stand having to create fifty files do to something like a database wrapper, and in Java that's effectively the only way to program.
Took me a few reads but it's better stated "six different classes". At first I was confused about why you rely on `java.io.File` for business logic.
So, if I'm stuck on JVM, what's my FP alternative that compiles and runs comparatively? Clojure? Scala?
If I'm stuck in JVM-land, Clojure followed by straight, modern Java would be my choices.
Because it is principled but also very pragmatic.
As for Scala 3, it's still years away, if it ever comes out. And when it does, there's little reason to think its goals will be different from what Scala 2 was (an academic language) since it's the same team as Scala 2 writing it.
There's absolutely nothing wrong with that, by the way, I love studying all the advanced concepts that Scala has pioneered over the years.
But it's also the reason why it's largely in decline and why Kotlin has taken the industrial world by storm: because it is a pragmatic language.
Kotlin doesn't have typeclasses (something you get as a side effect of Scala implicits), ADTs, or true pattern matching (along with exhaustivity checks). In combination, all of those allow for expressive, easy-to-read code that, in my experience, tends to have few bugs. Kotlin is a step backwards from that. It's still a significant step up from Java, however.
F# is the only other language I've used that I've found comes close. However it lacks typeclasses, and the large Java ecosystem.
I understand the value of higher kinds and I'm comfortable with Haskell, but it's pretty obvious to me why Kotlin is succeeding where Scala failed.
Sometimes, improvements in programming languages are reached by having fewer features, but Scala is a kitchen sink that was always unable to turn down features, just because their implementation would lead to more research papers to submit to conferences.
As a result, we ended up with a monster language that contains every single feature even invented under the sun.
So can you elaborate what do you mean by "failed"? Because it seems you are using a different definition of it.
"Sometimes, improvements in programming languages are reached by having fewer features, but Scala is a kitchen sink that was always unable to turn down features, just because their implementation would lead to more research papers to submit to conferences."
That's some different language you're talking about. Scala is built on a small set of very powerful, general, orthogonal features which cooperate nicely and allow to build most of the stuff as libraries. Its design is much more principled than Kotlin's. Kotlin has special features built into the language, that Scala needs just a library for.
Also there is no decline in Scala usage, and Kotlin doesn't exist outside its Android niche really. So "taking by storm" is a wishful thinking.
[1] https://stackoverflow.com/questions/34615947/why-does-kotlin...
This sounds like the root of your problem. If you want to do FP, and none of your coworkers want to do FP, then your problem isn't really the language.
If you're in a Java shop, maybe start by evangelizing FP rather than a totally different language/platform? It's possible to do FP in Java, and (IMO) it ends up pretty reasonable. But it's not the default habit for J Random Java Programmer, so they need training.
I work for a brand-name big company (I won't mention it here but I'll tell you if you email me) that hires incredibly talented engineers that are a lot smarter than me. The codebase I work on is around ~20 million lines of Java, and I've seen stuff in there that is so incredibly gross that a compsci professor would write "see me after class" if you submitted it.
Example: I once saw a piece of code doing this:
do {
// doing stuff
} while (false)
It took me about 10 minutes of digging into the code to realize that the person who wrote this was doing this so that they could add a `break` in there as sort of a makeshift `goto` so they could early-exit and skip all the rest of the stuff in the block. Needless to say, I was horrified.Why is it that incredibly talented engineers are writing awful code like that? It's certainly not incompetence; what almost certainly happened was that there was some kind of time-crunch, and the dev (understandably) felt the need to cheat. This is a direct consequence of the compiler allowing a bad design.
Microsoft DDK sample: https://github.com/Microsoft/Windows-driver-samples/blob/mas...
Microsoft MediaFoundation sample: https://github.com/Microsoft/Windows-classic-samples/blob/ma...
OpenSSL: https://github.com/openssl/openssl/blob/master/crypto/sha/sh...
And many others.
Just because code looks unfamiliar doesn’t mean it’s something wrong with the code.
I had this experience once in a Rails shop.
A simple database table mapped to a CRUD API endpoint would take from five to ten files. That amounted to about 500 lines, plus a lot of tests for each class.
I never really understood why programming became so verbose. In an ideal world I'd have a declarative API that mapped the table to the API for me automatically. In a realistic timeline I'd just use the traditional Rails approach and be happy. But the people working there preferred to use complicated patterns and a lot of boilerplate before they were needed, even though the project was perpetually late and riddled with bugs. I wish we could give a chance to simpler ways of solving problems.
Maybe we learn something each time the pendulum swings but as someone knee deep in C# at the moment, the quality of the APIs I have to deal with are far below those I was used to in Python (at least in terms of elegance and usability).
I'm not sure whether these flaws are inherent or whether it's possible to have one's cake and eat it.
I've had terrible experiences with complexity and verbosity in Ruby and Python codebases, which are dynamically typed. On the other hand, I worked with super expressive and simple to work codebases in C# and Haskell. And I had the opposite experience as well in other times.
It is absolutely possible to have the cake and eat it in this regard.
In fact I'd consider Haskell way more expressive than any dynamic language I ever worked with.
Compare these functions, one in JS and one in Haskell:
function F (x) {
var first = x.first;
var second = x.second;
return first + second;
}
vs. F :: (HasX a, HasY a) => a -> a
F foo = (x foo) + (y foo)
(I'm a little outta practice with both langauges, but my point will still stand)With the JS version, F can take in any expression that has the properties of `x` and `y`, while with the Haskell version, the type has to implement the typeclasses `HasX` and `HasY`. While the Haskell version is still better than something like Java because you can implement a typeclass without modifying the core datatype, it's still inherently less expressive.
I'm not saying that it's not worth it (cuz Haskell is awesome for everything but records), but it's still less immediately reusable.
This is a bit of shameless self-promotion, but I've actually written a framework that's MVC-ish that lets you create really declarative APIs. The first version is written in NodeJS that I actually deployed in production [1], and I have an Erlang port that's semi-complete that I've recently started hacking on again [2], with the whole crux of it that you should be able to simply declare the composition of your actions.
[1] https://gitlab.com/tombert/frameworkeyPromiseEdition [2] https://gitlab.com/tombert/Frameworkey-Erlang
((if (null? x)
;.....
))You'll notice that it's virtually unheard of to use any FP languages in critical software. Instead they use languages that lend themselves well to code reviews, static and dynamic analysis, model-based design and proofs, etc. Like C, Ada and some domain-specific stuff.
The kind of "correctness in the small" offered by Haskel through its type system can be obtain also in languages like C++, Swift and others. With the additional benefit of massive market share, teaching resources, mature tooling and so on.
Erlang powers around 40% of the world's phone networks; and if it's not mission-critical I'm not entirely sure what is.
For that matter, Whatsapp is also written in Erlang and Jane Street does trading applications in OCaml. Without making a judgement on whether or not they should, both Whatsapp and Jane Street create very large apps and have created successful businesses with FP.
> Instead they use languages that lend themselves well to code reviews, static and dynamic analysis, model-based design and proofs, etc
I can't tell if you're being serious; are you suggesting that Functional Programming doesn't lend itself to proofs? Really? Have you ever heard of Coq or Idris or Agda? They literally have modes to prove the correctness of your code.
What about functional programming doesn't lend itself to code reviews? I did F# for a living for two years and we had regular code reviews. I also used the .NET performance profiling tools which worked fine for F#.
> The kind of "correctness in the small" offered by Haskel through its type system can be obtain also in languages like C++, Swift and others.
Uh, no. Sorry, that's just flatly wrong.
Yes, static analysis tools are awesome, but you will never get the same level of compile-time safety from C++ that you will from Haskell or Rust or any number of functional languages. The type systems offer very little information, making it impossible for the compiler to shield anything.
In fact, by mentioning Erlang and Ericsson, you exhausted the only case supporting your point. Maybe if you tried hard, you could come up with a couple more. Now let's do the same exercise for the languages and tools I enumerated and it will take a long time until one runs out of examples.
WhatsApp is another perennial example in these discussions. I can accept it although there's nothing critical about a chat app - and once again it's rather an exception instead of the rule. Most chat applications are written in "not FP" programming languages and work just as reliably as WhatsApp.
In case it's not yet clear from the above, I believe that only tools which are used heavily in the industry deserve our attention, not obscure languages which haven't been put to the test and one off projects. The oldest trick in the FP argument book is finding some minor FP language to match any requirements put together by critics. So yes, I've heard of Idris and Agda - on HN - because barely anyone else uses them or talks about them. Coq is perhaps the outlier, because it was used to verify CompCert, but then again CompCert itself is used to implement a lot more things.
But Coq, Idris, Agda and so on are actually red herrings, because when people praise FP's correctness benefits, they refer to standard languages like Haskell, F# or OCaml for which there is in fact little proof that they have a significant effect on program correctness. Obsessively encoding information in the type system will reduce or eliminate some types of errors, but that's far from proving a program correct and really not that far at all from what's available in other standard, mainstream languages, for less effort, better support and a great ecosystem.
Even if the Erlang/Ericsson stuff is the “only case” (It’s not) I do not see how that makes my point less valid; Erlang was specifically design for systems that cannot fail. Telephones are just a good example of that.
Same goes for the other tools or languages I mentioned.
That said, and I addressed this specifically, when I say "OOP" in the software world, people typically think of Java, C++, or C#, and those are what I'm addressing specifically.
I suppose in the most technical sense of the word, you could argue that Erlang is OOP at some level, and Erlang is awesome, so if we want to play with definitions then sure, I'll concede that OOP is good, but until the industry as a whole agrees on these terms, and doesn't treat OOP as a synonym for "Java/C++/C#", I'm still going to say that I hate OOP.
It's easy to hate on OO because of something along the lines of it not being a neat mathematical formalism, which can facilely be argued as strictly a deficiency: if you don't look too closely, it certainly appears as only a deficiency.
I think a deeper look inevitably runs into two things:
(1) certain domains are more easily approached through spare mathematical formalisms than others. E.g. if the domain you're modeling is already most easily thought about in terms of compositions of mathematical transformations, you should probably model it functionally.
(2) Finding a declarative characterization of the results you'd like, or a neat chain of functional compositions which produce it, typically takes more work up front. (For many projects, the initial work up front is worth it—but for lots and lots of others, it's essentially over engineering.)
OO is often not 'ideal,' but frequently, solidly pragmatic.
As a paradigm, the aesthetic behind it reminds me of TypeScript's designers intentionally foregoing soundness of the type system.
This model works where it actually represents the real world: Mostly, in distributed systems.
In other areas, it just leads to overengineered piecemeal crap that is incredibly hard to understand. Where you can get control over what happens, you absolutely should get it. Don't act like your program was a thousand little independent components that have their own mind and lifes. Because it isn't like that, and if it was, there was no way you could actually get in control of these to make them produce a very specific outcome.
So the only reason why many OOP programs sort-of work is because programmers never actually respect the abstractions that they set up by defining so many classes and methods. To get the program to work, one needs to know very precisely what each class does in each cases. In the end OOP is just a terrible farce since there is no rhyme and reason for all these classes. It's needless bureaucracy, and prevents us from structuring programs in a more efficient and maintainable way.
Without OOP, we have the ugly organization of writing N functions that each dispatch M cases of code by pattern matching or switching on a numeric type field or whatever.
OOP lets us break these pieces of logic into separate methods. And then in the physical organizationof the program, we can group those methods by type. For any given type, we have N methods: each one implements one of those N functions just for that type.
This is a better organization because if we change some aspect of a type, all the changes are done in one place: the implementation file or section of file for that type. Or if a new type is added, we just add N methods in a new file or section; we don't have to change the code of numerous functions to introduce new cases into them.
Those who write articles opposing OOP never seem to constructively propose an attractive alternative for this situation.
It is this attractive program organization which swayed developers toward OOP, or even full blown OOP evangelism. It's not because it was hyped. OOP has concrete benefits that are readily demonstrable and applicable.
OOP is what allows your operating system to support different kinds of file systems, network adapters, network protocols, I/O devices and so on.
It's unimaginable that the read() system call in your kernel would contain a giant switch() on device type leading to device-specific code, which has to be maintained each time a new driver is added.
With OOP I can add a new datatype easily, but when I want to extend the behavior of that type I now need to go to M different places. With a functional style I only need to do one. You're open on types but closed over behaviors. Functional styles are the opposite.
In some sense, I would even go as far as saying the idealized 'UNIX philosophy' is a degenerate example of this. We have a very limited set of types (the file) and a bunch of independently implemented behaviors. Imagine implementing sed or grep on a per-file (or per filesystem) basis.
These both get really interesting when you consider libraries/user extensibility, since unrelated actors could now add either new types or new methods. Most languages just punt on this by banning one or the other.
A pattern matching style would allow me to add a new sql() system call to query into the filesystem. Look at how much trouble there is adding new features to CSS, TCP, Java, etc trying to coordinate among so many different actors.
Or consider the case of a programming language AST. I can make a pretty printer, an interpreter, an optimizer, a type checker, a distributed program runner. But trying to do that with an OOP style is much harder for a large AST.
At the end of the day, we have NxM (type, behavior) pairs and there are pros/cons to each way of slicing them.
Very true. It's a practical solution to a complex problem. However, when systems get complex, it becomes very hard to find the right object / type to bottle up logic. Perhaps, then, a mix of OO and functional is the solution.
The problem is that OOP is a slate of something like 18 characteristics and no language ever picks the same choices.
That having been said, the big problem with (especially early) OOP is that "Is"/"IsA" (aka structural inheritance) is the primary abstraction. Unfortunately, "Is"/"IsA" is a particularly lousy choice--practically anything ("Contains" or "Accesses" or ...) is better.
Most of the modern languages designed in the past 10-20 years reflect this--"Traits"/"Interfaces" seems to be what everybody has settled around.