5 ms·
> I'm sure someone will be writing "Functional Programming - The Trillion Dollar Disaster" I'm sure no one wouldn't. From my experience of working with GObject
by ernst_klim 7y ago
> I'm sure someone will be writing "Functional Programming - The Trillion Dollar Disaster"
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.
- ernst_klim 7y agoA simple example on how open recursion is harmful. Consider we have a object which computes the factorial: 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.
- fauigerzigerk 7y agoHere's a functional programming version of a similar bug: 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)));
- protomolecule 7y agoA simple example on how higher order functions are harmful. Consider we have a function which computes the factorial: 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?
- ernst_klim 7y ago> 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).
- millstone 7y ago> 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 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!
- ernst_klim 7y ago> you impose the restriction of knowing all logic in advance. > 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.
- millstone 7y ago> Changing the behavior of the codebase without any clue of how it works is nearly always leading to bugs and errors 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.
- ernst_klim 7y ago>If you insist that all involved software components be in "the codebase," 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.