In other words, I don't do hype or fads anymore.
In other words, I don't do hype or fads anymore.
Because we really ought to go that way (more proofs like Idris/Agda).
> “Simplicity is a great virtue but it requires hard work to achieve it and education to > appreciate it. And to make matters worse: complexity sells better.” > ― Edsger Wybe Dijkstra
Close! We have "Lies, Damn Lies, and Statistics".
Tongue-in-cheek aside, it's one of those things where the more good you do the more bad you do. If X has a trillion dollars of good impact, it'll more trivially have a few million of bad impact at least. It's like a weird inverse Amdahl's law.
OOP definitely solved a huge class of issues for a lot of people better than the "average state-of-the-art" at the time, and introduced a lot of people to even reasoning around concepts like encapsulation and abstraction.
Was it the best thing ever for all time forever and ever? Did it have contemporary ideas that could have been perfect forever?
Sure. Does that mean it did more harm than good. Questionable.
proofs are not everything in software. what do you do if you're handed a massive code-base in idris and are asked to make a change, a change that requires jumping through all sorts of hoops due to the type system? constrained type systems are fine but along some vectors of software development, they actually increase brittleness. maybe not brittleness at run-time but at develop and maintenance time.
I'm sure no one wouldn't. From my experience of working with GObject, C++, smalltalk and CLOS, I dare say that OOP is an inherently terrible idea.
OOP is harmful because it mixes two concepts you don't want to be mixed: open recursion and encapsulation.
OOP forces you to model your code as a state machine, where the transition logic is obfuscated (OOP people call it abstraction for some reason), hidden behind virtual methods, inheritance and abstraction.
This means you send a message to an object, and that triggers a chain of state transitions which is totally hidden (aka object could call a parent's method, parent's method could call some of the virtual methods, which sends message to some other object and so on).
This terrible implicitness makes your code obfuscated and impossible to debug, especially in presence of locks and parallelism.
Hence this recent trend of hatred towards inheritance and OOP in general.
Now, regarding Functional Programming. If you would draw an axis where to the left would be implicit paradigms, and to the right would be explicit ones, Procedural programming would be in the middle.
Procedural programming is very explicit, all state transitions are obvious, safe those which exploit side effects. Side effects is the only thing that make Procedural programming implicit (and buggy). OOP is significantly more implicit that procedural programming, because even the logic is obfuscated with virtual methods and late binding/open recursion.
Then FP on this implicit->explicit axes would be at the explicit pole, since FP tries to make the effectful code explicit, hence it's inherently good and less buggy, while OOP code is inherently more buggy. Each bug I meet in Gstreamer or our C++ codebase is extremely hard co comprehend and debug due to these obfuscated state transition, while debugging our OCaml codebase is a breeze.
I wouldn't say that message passing is always a bad thing, it's good in one case: when the logic is not known in advance, for example when you have a distributed system where nodes work independently and send message on occasion (aka actors). Although this case has its own underwater rocks.
class computer = object(self)
method factorial x =
if x = 0
then 1
else x * self#factorial (self#pred x)
method pred x = x - 1
end
Now, somebody redefines `pred` class computer_enhanced = object
inherit computer
method pred x = x
end
Now factorial simply hangs in infinite loop (until stack overflow). This example is very trivial, but when you have dozens of methods, mutable variables, locks, parallelism, and you redefine a virtual method (aka adding a recursive call changing the state transition graph) without knowing precisely how other methods work, this ends with annoying bugs, deadlocks and very hard to debug. function factorial(x, pred) {
if (x == 0)
return 1;
return x * factorial(pred(x), pred);
}
function makePred(x) {
return y => y - x;
}
console.log(factorial(5, makePred(-1))); let rec fact(x, prev) = if x <= 1 then 1 else x * fact(prev(x), prev);;
let factorial(x) = fact(x, fun x -> x-1);;
Now, somebody passes different function as `prev` let enhanced_factorial(x) = fact(x, fun x -> x);;
Now enhanced_factorial simply hangs in infinite loop (until stack overflow). This example is very trivial, but when you have dozens of functions taking other functions as arguments and those in turn are taking other function as arguments, and you try to alter behavior by passing a different function somewhere without knowing precisely how other functions work, this ends up with annoying bugs and very hard to debug.Would you agree that this example makes little sense?
Is it really different from yours?
Of course it is.
In your example you are explicitly breaking factorial. In my example I'm just defining `pred`, and it's unexpectedly and implicitly breaking factorial.
Behavior of the factorial depends on explicit argument, behavior of the factorial method depends on implicit recursive call.
It's the same as having a global variable and a locally passed argument.
let x = ref 0;
let f () = x + 1;
x := 42;
let = f ();
is not different from let f x = x + 1;
let y = f 42;
in a trivial example, but in a complex example with a complex state, the implicitness of the former would cause bugs.Same with open recursion, your code simply mocks a trivial example. In a complex example where methods rely on state and locks, implicit redefinition of a mutually recursive methods causes bugs hard to debug and comprehend, since you can't say what exact virtual method is called (you shouldn't even know that, that's the point of OOP).
In all mainstream languages with OOP features methods by default are not virtual and cannot be overridden by a successor class. You cannot accidentally create an opportunity for breaking factorial by defining 'pred', you'd need to explicitly declare 'pred' as virtual. If you don't do it, factorial cannot be broken and there is no problem.
If we pretend that you did it, the question is how is it different from taking 'pred' as an argument in the functional example?
Non virtual methods has nothing to do with OOP, they are a mere sugar for function calling.
We are talking about OOP, which is defined by late binding/open recursion, i.e. virtual methods, and the perils of open recursion.
> You cannot accidentally create an opportunity for breaking factorial by defining 'pred', you'd need to explicitly declare 'pred' as virtual. If you don't do it, factorial cannot be broken and there is no problem.
Ok, it seems you are confused by the trivial nature of the example, here is a real world example:
https://gitlab.freedesktop.org/gstreamer/gstreamer-vaapi/mer...
A decoder implementation calls GstElement's `change_state` method, which calls (through the long chain of calls) child's decide_allocation which calls parent and so on and so forth and in the end you have a deadlock.
This is pure OOP, where each Class provides needed virtual methods for state change, buffer preparation, allocation etc, and they calling each other in an obscure recursive manner. This leads to fancy implicit bugs if you calling methods without understanding how they work and where they are implemented, which is impossible, since methods could be reimplemented later.
> If we pretend that you did it, the question is how is it different from taking 'pred' as an argument in the functional example?
How do I know that `decide_allocation` method is calling a method I'm going to reimplement? How do I know that decide_allocation would be reimplemented in some future decoder?
In your example you explicitly redefining factorial. In my example I'm just redefining pred without caring about factorial and not even knowing that factorial calls pred (abstraction, remember?).
By this logic virtual functions are mere sugar for tables of functions and as such have nothing to do with anything besides functions and tables.
"We are talking about OOP, which is defined by late binding/open recursion"
Oh, well, definition of OOP is a moving target, everyone defines it as they like, but when we are talking about trillion dollar mistakes, OOP certainly implies C++, Java, C# and such simply because Smalltalk and CLOS have been used on the far smaller scale.
Having established this, I'm sure you would agree that saying that OOP prohibits or discourages non-virtual functions is unreasonable. It does introduce virtual functions, but like every tool, they shouldn't be abused.
"a real world example"
Looks like a poor approach to concurrency or an example of abuse of virtual functions, or both.
"How do I know"
Well, a method you are going to re-implement needs to have a clearly defined contract. If it doesn't, it is either not supposed to be reimplemented or the codebase is of poor quality.
You can look at the set of virtual functions of a class as an interface. In the case of a plain interface you wouldn't ask 'how do I know how this interface is used?' - you don't and you can't, you just implement the contract. The same with re-implementation of virtual functions.
"In your example you explicitly redefining factorial"
No, I'm defining a new enhanced math function that reuses some existing code without understanding of its contract. Just like you do in your example :)
No, open recursion is the only thing that makes OOP a distinct paradigm. Without `self` you don't have OOP. [1]
> No, I'm defining a new enhanced math function
Yeah, no, you are redefining factorial in terms of different `pred` functions, and I'm simply defining `prev` without redefining factorial.
[1] https://journal.stuffwithstuff.com/2013/08/26/what-is-open-r...
I'm not arguing that open recursion is part of OOP definition, I just disagree that C++ and C# aren't OOP since they have it as an opt-in feature.
You yourself happened to mention encapsulation as part of OOP in your original comment.
While you can call bundling data and functions together a sugar, limiting the scope of data to class' methods is essential.
Consider implementation of a hash map. Isn't it nice that the state is accessible only to {contains, get, set}? After auditing these three functions you can be sure that the invariant is never violated. How would you ensure that in FP?
Thanks for the link, I myself happened to explain the complexity of virtual function contracts in the same way, nice to have it written down in case I need it again.
You can have the same with an abstract data type and a set of functions that operate on it.
Only functions defined in the same module as the ADT can access its internals, so inspecting those functions would yield the same assurance you’d have in the OOP version.
Arguably the FP version would be easier to inspect, because all data would be local (passed as a parameter), while the OOP version could have “semi-globals” in the form of instance variables.
What's more, encapsulation (data-hiding) don't seem to be an integral part of FP and Haskell's modules seem more like an exception than a rule.
For example, listen [0] to the author of Clojure:
"Fogus: Following that idea—some people are surprised by the fact that Clojure does not engage in data-hiding encapsulation on its types. Why did you decide to forgo data-hiding?
Hickey: Let's be clear that Clojure strongly emphasizes programming to abstractions. At some point though, someone is going to need to have access to the data. And if you have a notion of "private", you need corresponding notions of privilege and trust. And that adds a whole ton of complexity and little value, creates rigidity in a system, and often forces things to live in places they shouldn't. This is in addition to the other losing that occurs when simple information is put into classes. To the extent the data is immutable, there is little harm that can come of providing access, other than that someone could come to depend upon something that might change. Well, okay, people do that all the time in real life, and when things change, they adapt. And if they are rational, they know when they make a decision based upon something that can change that they might in the future need to adapt. So, it's a risk management decision, one I think programmers should be free to make."
[0] https://gist.github.com/rduplain/c474a80d173e6ae78980b91bc92...
My point was simply that the nice property you described can be achieved by using ADTs, and you don't need OO to have that.
Should be: is not a part
No, you are not simply defining `prev`, you are creating a new class using `computer` as a base and supplying all its methods with the new definition of `prev`.
Let me complete your example:
class computer = object(self)
method factorial x =
if x = 0
then 1
else x * self#factorial (self#pred x)
method pred x = x - 1
end
class computer_enhanced = object
inherit computer
method pred x = x
end
obj_computer = new computer
obj_computer.factorial(5) //still works
obj_computer_enhanced = new computer_enhanced
obj_computer_enhanced.factorial(5) //stack overflow
And the same with my example: let rec fact(x, prev) = if x <= 1 then 1 else x * fact(prev(x), prev);;
let factorial(x) = fact(x, fun x -> x-1);;
let enhanced_factorial(x) = fact(x, fun x -> x);;
factorial(5);; (*works*)
enhanced_factorial(5);; (*stack overflow*)
In both cases the old `factorial` works and the enhanced one fails.Does
class computer_enhanced = object
inherit computer
method prev x = x
end
tells you that factorial definition changed and that it's dependent on prev? No.Does
fact(x, fun x -> x)
tells you that this function's behavior depends on `fun x -> x`? Yes.If you call your class `computer` and put a bunch of unrelated methods in it, then, yes, you are in big trouble, but you can abuse anything - you can create a data structure with hundreds of data members and make all your 'pure' functions take and return it.
To sum it up: I see your point, but I don't think that it's a fatal flow of OOP, it's just a way to abuse it.
In case of Java I agree with ernst_klim's critique, to me it's a poor language design choice. Dynamic languages are a mess anyway, but that's just my opinion :)
However you can still terminate a virtual chain, or not offer it at all with the final modifier.
Ruby, Python, and Java (among others) are relatively mainstream, but allow this (the former two have ways to do this with individual instances as well as child classes.)
Phrasing it differently, if you avoid message passing, you impose the restriction of knowing all logic in advance.
That's a grim constraint. Now your software is not extensible by users. It cannot evolve with its dependencies, like the OS. It is static, frozen.
This is not a small thing!
> That's a grim constraint.
That's the only way of writing a least buggy code, at least in my experience. Changing the behavior of the codebase without any clue of how it works is nearly always leading to bugs and errors.
> Now your software is not extensible by users.
What? Of course it's extensible, you could reuse ML's module as well, it just imposes valuable restrictions, aka, if you have a mutually recursive functions, you can't simply add another mutual call, which is imho a good thing.
Just look at OCaml's mirage OS, where there is alot of code reuse, and one modules extending others.
If you insist that all involved software components be in "the codebase," then you couldn't write this website, or the web browser you're using to read it, or the OS that my browser is running on.
> What? Of course it's extensible, you could reuse ML's module as well,
Users, I said. Plugins, scripting, OS, etc.
Functional languages tend to be pretty bad at this stuff. For example, "call a function if it exists at runtime" is trivial in JS and Python, but hard to even formulate in Haskell.
Where did I insist on that? Again, I could reuse ML modules just fine, as simple as I could reuse Objects.
> Users, I said. Plugins, scripting, OS, etc.
I'm a mirage OS user and I extend it quite fine, I could extend filesystems, block devices, write my own plugins to it. Why would I need OOP for plugins and scripting? Why can't I use modules instead? You don't need open recursion for extendability, a decent module system is fine enough.
> call a function if it exists at runtime
It's a very bad practice leading to calamities. A good half of Gstreamer bugs I've met were like that: use one function if it's defined or use another code path otherwise.
This is exactly an explicitness I'm talking about, an obfuscated control flow, and I don't want to work with the codebase which employs that.
Late binding could be easily emulated in Haskell/OCaml with environment (hashtable or map) of functions. People just don't do it because it is malicious.
Dynamic binding was introduced to allow code sharing. (With concomitant DLL hell which is still better than the hell of patching statically linked packages.)
Until FP grows some sort of schema (for type and functional safety) + dynamic loading functionality, it is dead as disco. As in impossible to use or you have to run through C or C++ making it an integration overhead.
Native level function interface is the killer feature. It is why C, C++, secondarily Fortran, Java and C# are alive. It is why Haskell is not even close to being as popular as Lua or Python or Ruby or perhaps even Rust. OCaml is better but it just drops all your warranty when native code is involved.
I don't think enough credit is given to how Functional Programming as a paradigm has shifted and changed over the years. It's grown and developed a lot.
There was definitely a time when having first class functions and functions as params was a big focus of Functional Programming. It opens the doors to a lot of expressive idioms. The inclusion of closures was an important part of this. This wave was definitely all about expressability more than structured reasoning. I'm talking lispy homoiconic macroy goodness.
Later Functional Programming thought was focused in the direction of ML/Haskelly/OCamly languages with a stronger focus on function purity, immutability, side-effect management, type inference, abstract algebra, and static guarantees of program structure and correctness.
Sure the ideas existed before, but they're hard to reason about until they're distilled into a paradigm/doctrine. And Functional Programming did that.
Like OOP, I think both waves of Functional Programming had a big impact on the programming landscape by introducing and popularizing a new vocabulary to a broader audience. The situational application of them will always come with experience and so every "fad" has the baggage of over application once it goes mainstream. OOP with Java, Javascript's framework explosion, 500 blog posts about monads, etc.