GUI programming is traditionally retained-mode. OOP is traditionally state encapsulation and mutability. OOP and retained-mode GUIs are conceptually similar, and Functional Programming and immediate-mode GUIs are conceptually similar.
Here's a good explanation of this: https://medium.com/@puppybits/immediate-mode-reactjs-7a6302c...
Being stateless and side-effect free is pretty fundamental to functional programming. If you can't replace a function call with its resulting value (without changing the meaning of the program) then it is not functional programming.
React is neither stateless nor side-effect free. Mapping it to concepts from functional programming is counter-productive. You get something that is neither fish nor fowl.
GUI programming surely wasn't retained-mode on 16 bit home computers, in fact that was one of the issues getting used to GUI based OSes event queues back in the day.
You say that as if it's the truth. It's not at all clear to me that's the reason OOP "won".
> These days people are taking a second look at functional because functional avoids state, which makes it easier to write concurrent code with no locks.
I think that's only a small part of the reason for the interest in functional programming.
I think there's a lot of value in the concepts of OO's message passing, and exposing an interface that encapsulates some internals. It's a tool I regularly use in FP even with immutable types
Imperative OO to me though is plain difficult to do correctly, and shouldn't be the primary way that your encouraged to write code. It's much more difficult to track dependencies and who touches what in an imperative class, and you end up digging around the file before you can even know what you have to keep in your head.
With FP, you generally have pure functions that take in some state and dependencies, and produce a new state. Everything you need to know related to how that function behaves is explicitly given to you, and it's trivial to test the code. You can do this in imperative OO too, but you're nudged in that direction by default.
I do think there are cases when imperative code and imperative OO make a problem easier to represent/understand and faster to execute, but I like to keep those sections small and easy to fit in your head, delegating most of the logic to functions. The imperative OO is mostly some glue to bring the business logic to life.
FP: 'state is hard, let's not do it unless we absolutely have to'
What's really interesting is Rust arrived at a similar destination, I assume as a natural byproduct of pushing for memory+thread safety. State isn't discouraged, but you get explicit owner(s) of data who lend it out to others who are readonly by default, and explicit when they will modify the reference you gave them.
I do, though, I think, find OO projects easier to understand. There's an opinionated convention about what code to save in which file (one file for interface of a class, one for implementation).
Sometimes I think it's this, more than anything to do with language semantics, that actually drives OO adoption. When you sit down with an empty folder and a text editor, it's really easy to know where the files go.
If GC was the distinguishing factor, OOP would have never gotten off the ground.
Let me make a meta-point. It's natural to find yourself with a thought of this form: I don't get why so many think ___ is cool.
There are two answers to this:
1. None of them know something you know, and you are right.
2. At least some of them know something you don't, and you are wrong.
Statistically speaking, 2 is much more likely to be true. So when you find yourself having these kinds of thoughts, which are entirely natural, you'll make better progress if you can formulate it as a good-faith search for truth and approach it with an open mind.
I don't get why so many think cigarettes are cool.
You're telling me they know something I don't about cigarettes?
* Cigarettes are stimulants and feel great when they first kick in, like a good cup of coffee.
* Smoking gives you a culturally-approved reason to take breaks throughout the day and go outside.
* Smoking is a social activity that provides an easy way to meet new people.
Of course, it is strongly chemically addictive too with serious negative health consequences, but the reason people smoke isn't only addiction or no one would ever start. I learned the above list by once genuinely asking someone why they smoke.
OOP as we know it today, which is not what the inventor of the term had in mind, likely won because it was easy to add it to C in a manner compatible with low-level optimizations that were critical in application code at the time, and less so now.
I dispute that one style is inherently easier to understand than the other, though each has certain problem domains where it maps particularly cleanly.
A couple points:
I'm not sure whether OOP really won: pretty much any language now has taken lambdas & higher-order functions from FP; before those parametric polymorphism (ML was there before C++ templates and Java generics); many sources will tell you to limit mutable state; and even algebraic data types are starting to get mainstream traction (Rust). Speaking of which, I believe Rust does not sell itself as an OOP language?
It is not at all obvious that OOP is easier to understand. Those animal/shape hierarchies are indeed very easy to grasp, and they are also useless. Even games have moved away from class hierarchies, which are both slow and too inflexible. They're more about ECS/data orientation now.
I'm not sure that "easier to understand" does lead to more maintainable code either. If you're talking about daily use, why not. If this is a learning curve thing (and in practice I suspect it is), then I think ease of learning and code quality won't correlate with each other — provided only people who learned the stuff get to program with it of course.
That's a wonderful description.
I don't think anybody had compared maintainability between similar projects in OO and functional style. Functional was not really a contender in this space.
Almost everybody who adopted OO migrated from imperative. The popular OO languages were imperative languages adapted to OOP - Visual Basic, C++, Delphi etc.
Actually, OOP won because OOP is easier for non-developers to understand -- those who were making policy and purchasing decisions back in the 1990's.
And the OOP we now call "OOP" (as realized in languages like Java and C++) is not at all the OOP that was first envisioned by Alan Kay and implemented in languages like Simula and SmallTalk.
There are still certainly benefits to using FP in JavaScript from a design, readability, maintainability sense, even if you are not concerned with concurrent code with no locks. A good FP design can be easier to reason about than an OOP graph of objects.
How is OOP easier to understand exactly?
If you are debugging the code that looks something like this:
class Child extends Parent {
launchRockets() {
this.launch(this.rockets);
}
render() {
this.score = this.getScore();
return `<span>${this.score}</span>`;
}
}
How do you know which part of your code contains implementation of this.getScore method? How do you know which part of your code is responsible for the number of this.rockets and how many methods in how many classes can change it? How do you find out which part of your code is going to call the launchRockets method, and whether this will even be this class's method or one of its children's? How many hops across your codebase do you need to make to figure all this stuff out?How is this style any simpler than functional?
You place the cursor at 'getScore' and press F12. This problem (calling functions defined god-knows-where) is not unique to OOP as opposed to FP.
> How do you know which part of your code is responsible for the number of this.rockets and how many methods in how many classes can change it?
You place the cursor at 'rockets' and press Shift+F12. This problem (modifying/passing data around god-knows-where and what parts) is not unique to OOP as opposed to FP.
> How do you find out which part of your code is going to call the launchRockets method,
See above.
> and whether this will even be this class's method or one of its children's?
Well, there is Ctrl+F12 that shows you different (re)implementations of the method.
> How many hops across your codebase do you need to make to figure all this stuff out?
About three to four. Again, this problem is not unique to OOP as opposed to FP: with FP, when your 'foo' function gets a 'frob' function passed to it as a parameter, it's anyone's guess what actual function it may be. To find all possible cases requires you to find all call sites of your 'foo' function, but that's non-trivial since it is most likely has also been passed around as an argument under different names.
Well, part of that problem is solved by explicitly importing functions from individual modules, at which point you know where exactly your function is coming from.
> when your 'foo' function gets a 'frob' function passed to it as a parameter, it's anyone's guess what actual function it may be
Part of it is probably solved by static typing. The other part is that it shouldn't pose a problem, much like you probably don't have a problem passing all sorts of frobs to your map-s, and your filter-s, and your forEach-es, and your then-s
Do you really never need to see what a passed-in function actually does in your FP code? Never need to see what type an argument is in our fancy Haskell or Hindley-Milner-typed language?
In FP, I tend to look at implementations to understand what they'll produce
In OO, I do this, but I also need to know _how_ since they might be changing the current context I'm working in. It's not enough to know that X method produces Y, I also need to remember that calling X implicitly modifies Z when I'm referencing Z
In an FP environment X can't modify Z outside of his context, so I don't have to care about how he produces Y. The cost of this is that someone needs to own Z and functions need to additionally return the new Z, but I think that explicit hierarchy helps in guiding towards a more understandable architecture
I'm hesitant to say always, but I can say:
- in imperative code I never feel comfortable without inspecting the passed in function
- in pure functional code, I rarely feel uncomfortable taking a pure function at it's word
60+, and OOP has been around for 50+.
> OOP won against functional because OOP is easier to understand and leads to more maintainable code.
OOP didn't “win”, it had a period of intense popularity starting in the late 80s and starting to fade around the mid-00s. And that was largely path dependence and building on C; had someone put the kind of effort into C-with-lambdas instead of Stroustroup's C-with-objects, we might have had a couple decades where the most popular languages were all C-descended impure functional programming languages rather than C-descended class-based OOP languages.
If by functional you mean higher order as in the title and not what is commonly known as FP - it's never been true. For example, a function that has no outside side effect and that calls an anonymous function provided as an argument is way easier to understand, reason about and maintain than its OOP equivalent of creating an object and calling three methods in specific order mixed with your code.
> These days people are taking a second look at functional because functional avoids state, which makes it easier to write concurrent code with no locks.
Avoiding state doesn't make it easier to concurrently touch shared memory without locks. Event loop style concurrency simply doesn't touch shared memory concurrently, so it doesn't need locks and doesn't have all those multithreading problems. It can still mutate state left and right, higher order programming just allows to map intent better to the code, making "sequences look sequential" and all that.
Actually solving problems concurrently with high performance today generally requires ditching shared memory based synchronously communicating concurrency models, leaving async communications of isolated entities where locking problems and overhead don't exist. But it has nothing to do with functional programming, OOP, higher order programming.
With the revival of dynamic languages though, people heavily programming in JS, Ruby, Python, etc. Performance is no longer a concern of people. So now people are motivated by what makes the act of programming as convenient to the programmer.
When it comes to that, it turns out a lot from FP does make things simpler for the programmer. It started with data transformation, I think helped by the appearance of LINQ in C#, which first exposed people to functional data transformation. Then Java got Streams, and everyone kind of followed suit in introducing some higher order functions and map/reduce/filter and all that.
Also: callbacks. As asynchronous behavior became more popular, there was a real need to lower the amount of verbosity and just code you needed to type to program in an async style. So creating a class and an object for every little event in your app, to pass as callbacks to all async functions just got super annoying. So people introduced anonymous functions and lambdas, and added support for higher order functions that could be used as callbacks.
Once you've got callbacks and higher order functions, closures are a natural extension, you often want to pass a callback that will set some state elsewhere when done or other such things. So Closures were also added.
And here we are, with a full FP toolkit due to the fact that performance isn't a concern anymore and async programming is more convenient when your language has support for first class functions with closures and anonymous functions and lambdas.
At the same time, like you said, people started squeezing more out of their cores and their seperate IO controllers, so concurrent and parallel programming became more common. That forced people to look at the immutable characteristics of some FP langs, and so support for immutability was also added to a lot of language.
Once you're in the realm of immutability, Objects also become less useful, since there's no need to encapsulate and protect modifications to something you can't modify anyway. Thus once again, creating a class and a constructor and all that just becomes unneeded verbosity and extra code to write. So people wanted ways to go back to immutable value records and just write functions over them, etc.
All these things in my opinion combined for what is today the "resurgence" of FP.
I know a JS dev who gets paid good money to develop software, who thought the reason you shouldn't use floating-point to deal with currency was because "javascript is bugged".
Functional programming actually brings some benefits to backend code. To take advantage of the extra cores in a CPU you add threads, but if these threads are continually needing to lock in order to serialize access to shared state then concurrency is diminished. The more cores you have, the more threads you have, and then the more you have to lock. Functional style solves this problem by not having mutable state.
But when it comes to JavaScript these benefits don't exist. That's not to say there are no benefits at all to incorporating some functional style into JavaScript code, but wholesale replacement of OOP with functional style is not warranted.