C++, java and other oop languages have been battle tested, and enabled a trillion dollar economy.
Sure, others could do the same, but they didnt.
Google, msft uses c++, c# and a bunch of others and they are probably the most complex software developers in the world.
It works.
Can it be better? Sure. Rust brings safety guarantees, f# enables functional programming mindset, julia is good for data processing. But oop did bring a lot of to market. We are evolving, and our languages will also evolve.
I’d argue that until C++11 C++ was /objectively/ a terrible language and the only reason people used it (and continue to use it) is because it’s nearly 40 years old and therefore ubiquitous. C++ is a language that is literally famous for being famous. I know I certainly wouldn’t be using it if I had any other viable (high performance) language with the same ecosystem, support and tooling. It’s not “battle tested” it’s just too big to fail, and therefore a lot of smart people have dedicated a lot of effort to developing tools and standards that make it just barely passable for enterprise use.
C++ is part of my favourite languages list, despicte some of the flaws, which I also criticize.
I belive that if Java and .NET actually had a proper AOT toolchain since day one, instead of being commercial only (Java's case) or doing basic optimizations (NGEN), there would be even less use cases for its use.
I concur. I used to work with C++98 (roughly, we had some weird levels of compiler support for some of it) and it was utterly foul. The only reason to use it was that there weren't really any better alternatives at the time. We needed something relatively manageable with really high performance, and that's what we got, but my goodness was it a mess in practice.
Modern C++ is much better, unfortunately there's stuff from the past they can't ditch without breaking compatibility so it really isn't much better - yes it's got better tools, but nothing's going to make you use them unless your compiler has some clever analysis features you can turn on and do things like make raw "new" an error.
I haven't written C++ code since the 90's, but I enjoyed writing code that way. I've also written Java code and C# code (in the late 90's and early 2000's). I enjoyed C++ at least as well as those. Some things are better, some things are worse. I've been doing some Rust lately and probably I would reach for that before I would reach for C++. There's a lot I don't like about Rust. I think it would be great if someone designed a language in about 5 years from now that contains all the things learned about Rust, but was designed from the beginning to solve the problems elegantly. Still, it's a fun language to use.
For me modern C++ is interesting, but it has even more of the problems that Rust has. There are lots of good ideas that help fix problems we've seen over the last 40 years or so (man, I'm old...), but I think unless you are already a C++ programmer it's probably more convenient to start with a language that was designed from the beginning to solve those problems. If someone paid me, I'd very happily write C++ code again, though.
Edit: Was surprised to see someone down vote the parent. Perhaps not realising that Steve Klabnic might legitimately be interested in how Rust can be improved ;-)
Ha ha! Before I answer, I should say how much I appreciate the work you do. It really is awesome.
I've been a bit clumsy with my words, though. It's not so much that I know there is a more elegant way to do it as much as I'm left with that feeling. For example the ? operator. It's incredibly useful and makes the code much less clumsy. But I'm left thinking, "Why do I need this"?
I've got a list of actions, some of which could fail and some of which can not fail. I want to exit early if something fails. Is there another way I could represent this? For example, something like a "do" notation. Then each statement essentially needs to be a monad. I'm not necessarily keen on that, but I think we're missing an abstraction somewhere. The set of functions that all return the same type, describe a monad... Why am I mixing functions with different return types, I wonder...
The thing is, I've got this extra notation that I have to learn about that does something useful. It just appears to me that I wouldn't need that extra notation if I could abstract the execution differently. Maybe that's a pipe dream, but it gnaws at me :-) And, of course, if we had it, the result wouldn't be Rust -- you'd need a completely different language to support that way of looking at it. Just to get rid of ?. Yeah, not worth it for Rust, but I think still worth thinking about.
Sorry for the rambling nature of this post :-) Just to leave you with a more concrete example that's not about Rust. I actually like many things about Javascript. One of the things I found really elegant about JS is that there are no private or public variables. If you want something to be private, you just make a closure. There is no need for a "private" keyword or a "public" keyword. There is no need to build in special code in the interpreter. It just works because that's the way it works.
Another thing I found really elegant about JS (though, unfortunately not executed as far as they might have) is the way classes are represented. You write a function that returns a dictionary/struct. That's a constructor. But it's also pretty much everything you need in a class. A class is just a function. Although it's a bit bizarre, functions are also dictionary/structs, so if you need class functions, just add it to the function. I'm not so keen on how they implemented prototypical inheritance, but it was the 90's so I'll cut them some slack ;-)
So when I say "elegant", what I mean is that the form of the language dictates its function. With Rust, I find that we have quite a lot of stuff like the ? operator. It's super useful and leads to better code, but it's also kind of tacked on. If I were going to design a language that had similar utility, but didn't need the operator, what would it look like? Maybe there is no more elegant solution. However I kind of think that with another 5 or 10 years of work with Rust people will start seeing the kinds of workflow that Rust engenders and will probably discover abstractions that fits that workflow better -- much in the same way that Rust has done with C++.
> Was surprised to see someone down vote the parent.
It happens, it's just the internet. I have enough karma, haha.
> Before I answer, I should say how much I appreciate the work you do. It really is awesome.
Thanks! It's cool, I was just legitimately wondering. As you said sorta, it's helpful for me to understand people's perspective. If you'd like, I can give you some explanations here, but I wasn't asking to try and correct you, I was interested in your perspective. Thanks for sharing it :)
> literally famous for being famous [...] not battle tested
> “[no other] viable high performance language with the same ecosystem, support and tooling”
This comment strikes me as deeply contradictory. I have qualms with C++ as any other C++ programmer, but saying it’s famous for being famous is just ridiculous.
C++ has been used to operate nuclear submarines, airplanes, pacemakers, financial systems, ... over the last few decades. I’d like to know what you consider “battle tested”.
Using an OOP language does not mean OOP programming. There is plenty of java/c++/c# code out there that use classes as glorified namespaces and stick to POCO/POJO objects. From my own experience I'd say that this describes the best and easiest to maintain code in those languages.
The hardest to maintain code is almost usually a mess of inheritance and generics/templates by someone far to "clever" that thinks OOP is the goal and not the tool. More often than not an inheritance hierarchy is basically (not literally) an abstraction over if/switch statements, except the logic will usually be spread out over multiple files and it's impossible to track all these potential branches in your head because they're purposefully obscured from you.
This is a good point, however it also creates code that has no branch points (ideally) and is therefore easier to reason about. It is virtually identical to the situation of passing a flag to a function and branching based on the flag. This leads to a function that is more complex. You can move the branch above the function and write 2 functions, one which handles each case. But these 2 functions are inherently coupled (and may even share code). So it's a tradeoff.
The OO style of programming also often moves the decision point. Instead of having an if statement above the 2 functions, you have the if statement at the construction of the object. If you happen to be writing code where you are doing mostly the same things, but certain details change in certain situations, you can group the differences into objects. If you are familiar with pattern lingo, this often happens if you are doing something that lends itself well to a template pattern. You can then use a bridge pattern to ensure that there is only ever 1 branch, instead of adding a decision point every time you want to vary the behaviour.
Where I find people (reasonably, IMHO) disagree is that having the decision point at the place where the behaviour diverges allows you to see the logic of the situation right at that point. If you have a bug in the code related to that, then it's very easy to figure out where it is, because all of the logic is local.
On the other hand, if you are refactoring the code, or changing the behaviour across the system, then it is very difficult to change because you have to search for all of those decision points. It's also harder to reason about on an architectural scale because it takes discipline (that may be lacking) to ensure that you always do things exactly the same way. You have a lot more flexibility because you can add all sorts of different logic to each decision point. The lack of constraint means that you have to inspect each case to determine if your overall change is going to work.
I'll be honest. I like OO. There are a lot of really diabolical ways you can write bad OO code, but there are lots of diabolical ways you can write any code. I'm familiar and comfortable with imperative, modular code. I'm familiar and comfortable with FP. I'm even familiar and comfortable with declarative code (I wrote some large XSLT systems). There are trade offs which every approach. It's easy to see problems in code that you are not happy with and then throw the baby out with the bath water. Having said that, I don't think there is a problem with having a preference of one style over another, but it's a good idea not to fool oneself into thinking that it solves every problem.
> On the other hand, if you are refactoring the code, or changing the behaviour across the system, then it is very difficult to change because you have to search for all of those decision points
The very best programmers optimize their code for readability. Most of our day is spent reading code.
That is very true. Reading code is also one hell of a way to understand the bigger picture, and opportunity to familiarize ourselves with a larger system.
> If you have a bug in the code related to that, then it's very easy to figure out where it is, because all of the logic is local.
More often than not I find this isn't the case. If we have an inheritance tree of (A -> B -> C) then you'll run into cases where the bug is in a object of type C but it's calling a method defined on type A, overridden in type C and accessing a variable in type B. So you have to look at why A and C diverged, where the value is set in B, what else calls the method on A, etc. You have to know the whole graph and the abstractions are trying to hide it from you.
This is particularly true the older code gets because all those little "just overload this function" adds up and it can't be refactored because it's impossible to understand.
Can anyone point to an IDE or GUI programmed from the ground up in an FP style?
The declarative simplicity of FP makes it a joy, but I find myself skeptical that flattening the OOP hierarchy into what would be a right jolly long list of pure functions will prove practical.
I'd call type classes a non-OO form of polymorphism, it doesn't become OO just because you have polymorphism (if that's what you're implying). I think of OO as late-as possible dispatch, whereas type classes are compile time only in most implementations.
I've been around long enough to remember when OOP was the savior who would free us from the previous generation of trillion-dollar disasters (spaghetti code, CASE tools, DFDs), and I'm sure someone will be writing "Functional Programming - The Trillion Dollar Disaster" in 10-20 years.
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.
Depends on what you mean with Functional Programming. Do you think we will ever see: "Math - The Trillion Dollar Disaster"?
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
Unless the tooling makes it trivial - which is still a long, long time away, if possible - I’m not sure there is a world where the increased cost of formal verification (mostly in terms of engineers who know what they’re doing) makes sense for things like line of business PHP/node.js websites, internal python apps, bash scripts, etc.
> Do you think we will ever see: "Math - The Trillion Dollar Disaster"?
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.
> Because we really ought to go that way (more proofs like Idris/Agda).
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 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.
A 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.
Here'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)));
A 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?
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 your example you are explicitly breaking factorial. In my example I'm just defining `pred`, and it's unexpectedly and implicitly breaking factorial."
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?
> by default are not virtual and cannot be overridden by a successor class.
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:
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?).
"Non virtual methods has nothing to do with OOP, they are a mere sugar for function calling."
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."
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.
> 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?
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.
But what I described IS an abstract data type and the concept of abstract data types appeared in CLU which is an OO language.
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."
Sure, not all FP languages provide encapsulation, and neither do all OO languages (e.g. in Ruby there's always some way to access an object's private parts).
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.
I think there is a difference between having a way to bypass encapsulation and deliberately not having encapsulation at all. Otherwise I agree with you.
"Yeah, no, you are redefining factorial in terms of different `pred` functions, and I'm simply defining `prev` without redefining factorial."
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.
`factorial` definition hasn't changed, what changed is its implicit parameter. The word 'inherit' tells you that all methods of the `computer` class are potentially affected, including `factorial` unless you can see in `pred`'s contract that it affects only `factorial`.
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.
Not in C++ unless a method is explicitly marked as virtual. IIRC C# behaves in the same way. In Java, D, all methods are virtual by default. Most dynamic languages also behave in the same way (due to pervasive late binding).
I haven't thought of Java. As the sibling comment says, it's the case for C++ and C#.
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 :)
> In all mainstream languages with OOP features methods by default are not virtual and cannot be overridden by a successor class.
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.)
Yes, that sentence was wrong and if you say that ability to override any method is the essence of the true OOP I would agree that the true OOP is a disaster.
> 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.
> 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.
>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.
Therefore people do not do dynamic binding at all, making this worthless for most kinds of system programming.
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.
In OOP those groups are called classes, and the fun is that you can instantiate them and call them objects. And within those objects you can set a state, so those functions operate against it. A nice feat of OOP is that you can also inherit logic and expand it.
In the wrong hands, it does. Easy to tangle OOP. But the point is each of the two paradigms has its own benefits, and should be used when best for the task at hand.
Indeed. Sometimes tho one just needs to pass a data set to a function, then the result the another, and so on, without the execution branches needed for figuring out the modularity rules and constraints of OOP. And that is where functional programming is relevant (and in math like scenarios, where speed and accuracy are key). But not as a rule of thumb, for everything programming.
The key to understanding classes, IMHO is that you are grouping functionality to make the code more cohesive. The style of grouping is to group the functions based on the program state where these functions make sense.
Often people go the other way around and write entity relation diagrams. They write classes to encode the data that they want to work with and put the functions that interact with the data into those classes. This actually leads to code that isn't OO (again IMHO). It actually more closely resembles modular programming. You have areas where you say, "I'm dealing with that data here". All of your instance variables are essentially global to that module. Especially when the classes become large, this becomes mostly indistinguishable from imperative code with global variables.
Instead, consider the parameters that the methods on your class use. Imagine that you had no instance variables and that you had to pass all of the parameters to your functions. Now group all of the functions that use mostly the same parameters. That's a class. The instance variables represent some state of the program and the methods work on that state. Afterwards you can make diagrams to see how these objects interact with each other, but it usually doesn't make sense to do it before hand (because otherwise you end up with bags of data and functions that act on that data as if they were global variables).
Or, more concisely: An class is not a table in a DB and you shouldn't model it the same way ;-)
Btw. no, this is not how OOP works. With the method I outlayed I have very easy time to achieve idempotency, the thing you suggested makes it harder because the objects have internal state and the method calls can depend on those.
OO is not bad. You can write bad code in any number of different ways. Whether or not you choose to write OO code depends on how you like the style, how your teammates like the style and what you are doing. If you want to be a good programmer, you should learn how to write modular imperative code, OO code, FP code and declarative code. It takes a long time and a lot of learning to be a good programmer.
As for what should start with, it doesn't matter in the least. Start with whatever you feel like, or whatever your friends are doing, or whatever is convenient for you. Keep an open mind and have the attitude, "Well, I'm going to do it all sometime so I don't need what I'm doing now to be the 'best'". Don't try to save time -- you can't do it, because there is no end point in learning that you can achieve in your lifetime. Just progress down the paths that interest you and try to enjoy yourself.
Focus on delivering solutions using what you know best. If OOP is doing the job for you, by all means stick to it. Code is a means to an end. In fact, these days, if I can be able to deliver a software solution to my clients without writing a single line of code, I will go for it.
I dream of a future where instead of following messianic paradigms, developers follow the mantra of “the most suitable tool or practice for the task at hand”.
I dream of a world where instead of finding the most suitable tool or practice for the task at hand, developers follow the mantra of questioning those most suitable tools or practices without blindly adopting them
Developers don't work in a vacuum, it's hard to just use a little bit of that paradigm here and a little bit of the other paradigm there.
You do need to plan your career and pick the winning paradigm/stacks because you need to invest significant amounts of time in mastering them.
No paradigm, OOP, FP, etc is "perfect". Yet they are all practical in the hands of competent programmers. Personally I don't particularly like C++, C# or even Java. I have seen way too many dreadfully implemented systems in those languages. BUT it is not the fault of the languages. Bad programmers can shoot themselves in the foot with any language, it's just a bit harder with some but not impossible.
In my experience, business applications are all about state. The state of an account, a customer contact, an invoice. Changing state is what messages / methods do. In a well designed and implemented system minimal inheritance gets the job done. And bypassing encapsulation is a big no-no. Polymorphism reduces cognitive load. A toString method should return a meaningful representation of the state of all objects in a well designed system, etc.
But fear not, the majority of banks, insurance companies, big business still use COBOL on mainframes. No danger of OOP there. It's all 1960s & 1970s programming with GOTOs and spaghetti code. And yes, those are the systems that manage trillions of "REAL" dollars everyday.
Some of the most literate, easy to comprehend and maintain, stateful, Gui-heavy desktop apps I've written / worked on have been detailed models of interactive objects, because it was appropriate for the problem.
In other problems (the classic web, data processing), it doesn't make as much sense to hydrate this huge, complex ecosystem of objects over and over just to throw it away again, or to end up with functions which are nonsensically bound to what is really just a data structure. Other operations are particularly recursive or parallel by nature and lend themselves to FP at least in part because of the work done in the FP community to handle those paradigms extremely well.
Of course a bad programmer will write bad software, no matter the language. But are you not taking this to the extreme?
Surely some programming paradigms are objectively worse than others for certain use cases? I've personally come to the realization that OOP is only useful in very niche cases.
I broadly agree, but this article is really poorly written. It takes every downside of OOP to extremes which make it really hard to take the author seriously. Yes there are problems with mutable state, even if you think you've encapsulated it. Yes there are problems with dependencies, dependency injection and the associated boilerplate. Yes there are hideous issues with concurrency.
I don't think OOP as we see it in most languages today is particularly helpful. I do increasingly feel that data structures and operations should not be muddled together, and the polymorphism strategy should be something more like Haskell's typeclasses/Rust's traits or Go's idea of interfaces.
I'm also sure that using those for a few decades would show us plenty of things wrong, and lead to something else.
We are still children in this world. There are no correct answers - but there are definitely some wrong ones, and the increasing realisation of that is evident in the popularity of languages like Go and Rust, and the development of C# is also interesting - increasingly new language features are FP features, not OOP features, and the language becomes increasingly hybrid. It will always be limited by being built on a fundamentally OO runtime, but it's interesting how things are changing.
This article is just not very good, the author keeps talking about a zillion different things without really focusing on the actual, underlying issues. But one thing it does say right away is that mutable state is not a problem as far as it goes, what's problematic is mutability plus promiscuous sharing. This is what makes it unfeasible to reason about what the code is doing.
Similarly, encapsulation (bundling code and data) definitely has its uses - in preserving class-wide invariants, or in providing a "common" interface that makes it possible to disregard the inner workings of some data structure, and interact with it in a way that's not dependent on the implementation. But in practice, implementation inheritance in OOP often encourages what amounts to breaking encapsulation and making it possible to violate class invariants, all in search of some dubious "code reuse". If you can't express your desired code reuse pattern via some combination of simple interface inheritance, delegation and dispatch over "sealed", non-extensible variants (the mechanisms that plain old composition/FP gives you) there are probably some underlying problems with it that you haven't thought through.
Of course the standards of programming improve continuously over time. Even FP and broadly "FP-like" programming is not what it was in the 1980s, and some of that progress was brought (if perhaps incidentally!) by OOP languages. This is to be expected!
Not really enough. Facile witty criticism doesn't add much to the discussion and lowers the level of discourse.
The same thought expressed with enough meat to chew on might provide a worthwhile addition to the conversation but such venears once pierced are often as solid as movie sets.
It's blocked for me, I have had 2 free reads... Any tips? I'd actually like to read these, the previous 2 were a let down and I never finished them even.
I don't understand, I actually though medium was sort of free (as in beer), who is getting this money?
Abstractions and design patterns aren’t what object oriented programming is about, you can change the paradigm and you will still have these (though you will maybe name them differently).
>are OOP, not procedural. They just lack the syntax sugar.
ADT's/"Object-based" programming (which is what you show above) is not the issue. The biggest problems occur when you introduce non-trivial inheritance/polymorphism.
> Really depends on what is your definition of "OOP" here
I smell a no-true-scotsman incoming..
>> Both Smalltalk and Erlang make use of OOP the way it was originally intended by its inventor, Alan Kay.
> ah, the bad definition of OOP then.
Message-based OOP is usually considered to be superior to inheritance by even OOP advocates, and even composition-based OOP by many. I don't see your evidence for "bad definition of OOP".
>Hint : most C apis that look like
> foo* foo_create(void);
> int foo_get_bananas(foo);
> void foo_print(foo, int, int);
> void foo_free(foo);
>
> are OOP, not procedural. They just lack the syntax sugar.
Not every API that uses a struct is OOP. If your foos use inheritance, virtual dispatch etc, then yes it's OOP. But usually it's just something like a struct to store state instead of having globals or multiple parameters even.
>> Linus Torvalds is widely known for his open criticism of C++ and OOP.
He's a pragmatist first and foremost, and the GUI libraries he uses (Qt) and the programmers that work on GUI code use C++, he said. I doubt he would write his own GUI framework in C++.
Medium (codebase A) dynamically interrogates the API provided by my browser (codebase B) as influenced by plugins (codebase C) linking the UI framework of my OS (codebase D) and then loads some polyfills provided by Babel (codebase E)... Which one is "the codebase?"
Your runtime environment is multiple components, each evolving independently! How do you write reliable software here? You can use static types but can't trust them, they can't predict the future and don't know the past. You want runtime interrogation and reflection. You want graceful degradation and dynamic polyfills. You need late bound interfaces. You need objects, not ADTs.
This I think is the heart of the OOP idea: software components realized in classes/objects/messages which can evolve truly independently. Drop the "THE codebase" assumption and some of OOP's strengths will appear.
I would love to see atleast a few large projects implemented with Haskell/FP before making such tall claims. Large here means few millions of lines of source code and more than 50 engineers working on the same project, a monolith such as a database or a message broker.
“Measuring programming progress by lines of code is like measuring aircraft building progress by weight.”
― Bill Gates
BTW: Are you considering Go to be OOP? Like it always is in the programming world everything is ill defined. Go has no inheritance from what I've read.
The quote is fantastic and it does bring out an interesting dimension. Not all parts of the code base are equally important. That is for another day. I believe you are missing the context. Allow me to explain.
Source lines of code is a proxy to the complexity of the problem in hand. With the assumption that the engineers will not unnecessarily write code just to fill up the code base or pass time, we can safely say that the number of lines of code for a database will be significantly higher than say a networking library. To write more lines of code, we need more engineers. And with more engineers, we uncover a variety of problems.
No accomplished Haskell programmer will agree that Go is a functional language. At least the ones that I know don't. I think FP programmers are not in agreement as to what constitutes a functional language, example Haskell vs Clojure vs Elixir.
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.
- Dijkstra
My point today is that, if we wish to count lines of code, we should not regard them as "lines produced" but as "lines spent": the current conventional wisdom is so foolish as to book that count on the wrong side of the ledger.
- Dijkstra
The required techniques of effective reasoning are pretty formal, but as long as programming is done by people that don't master them, the software crisis will remain with us and will be considered an incurable disease. And you know what incurable diseases do: they invite the quacks and charlatans in, who in this case take the form of Software Engineering gurus.
- Dijkstra
This article is just someone’s frustrated rant, poorly written.
Definition of OOP has changed over the years, early on it was a lot of focus on inheritance & polymorphism, but today it is mostly about composition, which is basically what you do in FP as well.
New inventive ideas like Go's & Typescript's interfaces flips the relationship between the caller & the callee. Or Rust's traits where you can expand your existing types with new behavior.
There is criticism about OOP to be said, but using Java as your strawman to invalidate OOP is not really a good argument, it is just one implementation of many.
Edit: removed the word dynamically from description about Rust traits.
I'm not going to defend the article's writing style, but Rust is effectively a functional language with controlled use of procedural-programming features. Not that much different from how Haskell places safeguards on the very same things. (No, I'm not saying Rust today is just as safe or just as elegant as Haskell - far from it! But it can get there with some work.) Even FP is not what it was back in the 1980s and 1990s!
Rust is not referentially transparent, does not model effects (any function can rm -Rf your drive), permits shared mutable state (RefCell, etc) includes statements not just expressions (break, return). Its most important safeguard is its affine type system, which is absent from Haskell.
Would it be too much to ask for a REAL real-world example? Unless you are already an expert in both paradigms you (I) have no idea what is the author talking about.
How is nullability supuerir in Functional?
How is error handling superir in Functional?
What is really meant by shared mutable state? (The example with a piece of papaer is equally useless as the explanation that "state is any temporary data stored in memory")
How are "erroneous abstractions" promoted by OOP?
What is the "low signal-to-noise ratio"
How is the state NOT promiscously scatered between objects in Functional programming?
How do you make "core parts of your application be stateless and pure" in Functional programming?
How is global state "the root of all evil"?
Could someone point me to a literature that can explains these questions to someone who don't know the answers already?
Particularly the chapter "Relational Databases" which compares to programming with sets in databases. This book doesn't tell you to program as if you worked with SQL, but draws inspiration from it and makes a comparison.
First I was programming in C and I was told C is bad and I must move to a better C -> C++. I listened and moved to C++. Then I was told C++ is horrible and I must switch to modern OOP langauges like Java and C#. I moved more than 15 years ago and I am now very productive when delivering solutions using C# and Java.
15 years later, I am now being told to dump my decade and a half knowledge and competitive advantage and learn a new programming paradigm? Miss me on this one. This time around, I will stick to what I know best. OOP has 80% of what I need and I know ways to deal with 20% of the problems. I will stick to it.
It took me only 2-3 days to learn F# but I feel I will be doing my clients a disservice if I have to start from scratch and start delivering solutions to their problems using a programming paradigm that I have little experience with.
No. Thank you. I will stick to delivering solutions to my clients using what I know best.
To be frank C# is a wonderful FP language. Try to minimise state (readonly structs helps) and see where it takes you. I recommend all programmers to go a programming paradigm course though.
I think as developers we should constantly learn about new approaches. Doesn't mean we need to use them blindly and follow the herd. Sometimes it's also useful to learn something to realize why one should not use it, and how the current approach is actually better.
That's why I learnt F#. Just to see how functional programming is going to help me deliver solutions faster to my penny-pinching clients. It was an eye-opener but I will still stick to functional programming until FP catches on. If it happens.
At age 37, I feel young and really wish that I will still be able to program at age of 50 and beyond.
Data dominates. the programming moves toward the data. from initial state to final state, the shortest linear distance between two points. Simplicity is the root of fast, stable and reliable.
`The Pure Function Pipeline Data Flow`, based on the philosophy of `Taoism` and `the Great Unification Theory`, In the computer field, for the first time, it was realized that the unification of hardware engineering and software engineering on the logical model. It has been extended from `Lisp language-level code and data unification` to `system engineering-level software and hardware unification`.
Whether it is the appearance of the code or the runtime mechanism, it is highly consistent with the integrated circuit system.
Using the input and output characteristics of pure functions, pure functions are used as pipelines. Dataflow is formed by a series of pure functions in series. A dataflow code block as a function, equivalent to an integrated circuit element (or board)。 A complete integrated system is formed by serial or parallel dataflow.
It has also been widely unified with other disciplines (such as management, large industrial assembly lines, water conservancy projects, power engineering, etc.). It's also very simple and clear, and the support for concurrency, parallelism, and distribution is simple and natural.
There are only five basic components:
1. Pipeline (pure function)
2. Branch
3. Reflow (feedback, whirlpool, recursion)
4. Shunt (concurrent, parallel)
5. Confluence.
The whole system consists of five basic components. It perfectly achieves unity and simplicity. It must be the ultimate programming methodology.
This method has been applied to 100,000 lines of code-level pure Clojure project, which can prove the practicability of this method.
I learned Python about 5 years ago, just for context, I know a bit of bash, some html/css etc. I don't really understand this piece. I use Classes in Python, put them in a module, I also put functions there. Some things are nice as a class, some things are nice as general functions. I like inheritance, otherwise I repeat things. Maybe I need to grow my project and run into limitation before I understand the viewpoints. Maybe anyone has to go through this? Should I feel bad about using classes/objects?
"""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."""
I'm getting sick an tired of all these OOP bashing articles.
X is terrible, Y is super! Fact is that everything has benefits and drawbacks, and so does OOP, Functional programming or whatever.
The strength about OOP is that it's practically the best to program most software. If you look for example at the SDL library written in C, it uses an OOP interface with handles to keep things nice and simple. Yeah that's right, OOP in C, why would anyone do that? Because it has benefits, that's why.
So why is most software nowadays written in OOP? Because it has the most benefits. You can sure find examples where functional programming would be better, but they are a minority.
So yeah, next time I would expect an article to properly make a comparison between benefits and drawbacks of both things that are compared. Not the usual "all drawbacks of X, but look at all benefits of Y". This kind of thing screams "Junior alert" to me.
Meantime, I'm developing my apps and games and applications in OOP, and absolutely loving it. Wouldn't trade it for anything else in the world.
SDL having an interface with handles has nothing to do with OOP and is simply the concept of opaque handles.
This is often seen in data-oriented code bases where a system behind the scenes is dealing with transforming data in a hardware-oriented and cache optimized manner.
Sure the article isn't super well-written, but it raises a fair point and shouldn't just be thrown away.
Just because something is the most used or standard does not mean it is the best solution. Eating junk food is very common, but you would hardly argue that it is the best way for a human to get nutrients.
You can use OOP all you want, and it's fair to say that the article should have been less biased (though he has a disclaimer that says he is biased), but there is nothing suggesting that OOP is practically the best paradigm to program most software other than the fact that it is the most used.
There is no doubt that using handles like that takes concepts the same like OOP, such as constructor, encapsulation, object methods, etc. Therefore moving this API interface to OOP would mean a straightforward 1-on-1 mapping. Claiming this has no relation to an OOP setup is therefore not correct. It would have had the exact same interface if it was written in OOP.
Maybe we are talking next to each other about what a good programming language is. For me as a professional programmer, it means to sell value. This implies that the companies I work for can make the most amount of revenue. Thanks to free market forces, we can assume that the most "optimal" programming languages survive. Remark that "optimal" is not expressed in terms of aesthetics or whatever, but is purely the productivity of generating money.
You can indeed claim that fast food is not healthy, but from a business perspective, it is a success story, and therefore also popular. I don't know how "healthy" OOP is, but it is hard to argue that it is not a business success story. And that is exactly what I offer to my clients.
Edit: And to be honest, I would have no clue how I could make the business case for any of the projects I ever worked on for not using OOP. These include games, consumer mobile apps, robotics and various business applications (smart client-server, etc).
Fair enough with that argument you can say it has a relation. I think what I was trying to say was that I don't necessarily think the authors of SDL were inspired by OOP when designing this type of interface.
Maybe we are talking next to each other about what a good programming language is.
Yes, I think we are at least in a sense. If we are speaking about programming languages from a business perspective we can agree that the language leading to the most productivity will - in a business sense - be the most optimal.
I think my point is we have been productive in spite of OOP and not because of it. OOP here meaning the way it has been since Java and not Alan Kay's original ideas.
When something like OOP becomes ingrained in an industry like it has been in software development in both the business and in academia, I'm not sure just waiting on the free market is going to do us any good if we (i.e. me) want to change it.
Personally I am vastly more productive in the non-OOP code-bases I have worked in as opposed to the opposite. This is a personal bias, but I am no the only one and there is a growing movement of moving away from it and often already completely gone away from it. So it's not just a case of the grass being greener.
I assume you are more senior than me, since I am not particularly senior, so you can again write it off as me being a junior. But it's a growing paradigm shift, especially in game and game engine development, which is what I do for a living.
Senior also means that I built up a certain way of reasoning and avoiding or working around common problems. So I might not be the best person to push a new paradigm shift ;).
You got me intrigued about being less productive in OOP. Can you share what exactly you are working on? My first guess would be that it has something to do with concurrency or parallelism? It especially interests me since it's game development :).
Senior also means that I built up a certain way of reasoning and avoiding or working around common problems
I see this in my current senior colleagues, who may be very OOP-oriented but they are incredibly productive because they are super experienced in their field and in the code-base. I 100% respect that type of seniority of course, because I have learned a lot from my seniors.
Regarding my productivity I think it is first and foremost related to how my brain solves problems. I especially like the idea of Data-Oriented Design, because I think of solving programming problems as three simple steps:
1. Some data comes in
2. Data needs to be transformed into something else (based on some business rules)
3. Data needs to be presented
So when I want to solve a problem I first define my data, then I define my transformation functions and then where they are used afterwards. It's not necessarily a purely functional approach, since they do change state as opposed to returning new state, but it has very little overhead in my brain. And as you mention concurrency, it simplifies it vastly because you usually know exactly who is using which data where.
I work mostly on engine development, so there's a lot of room for optimization and understanding how a lot of data is transformed every frame, which makes performance a super interesting topic for me. I can reason much more easily about performance in a procedural code-base than in an OOP code-base, so that might be part of my bias. :)
129 comments
[ 3.3 ms ] story [ 166 ms ] threadSure, others could do the same, but they didnt.
Google, msft uses c++, c# and a bunch of others and they are probably the most complex software developers in the world.
It works.
Can it be better? Sure. Rust brings safety guarantees, f# enables functional programming mindset, julia is good for data processing. But oop did bring a lot of to market. We are evolving, and our languages will also evolve.
I belive that if Java and .NET actually had a proper AOT toolchain since day one, instead of being commercial only (Java's case) or doing basic optimizations (NGEN), there would be even less use cases for its use.
EDIT: typo
Modern C++ is much better, unfortunately there's stuff from the past they can't ditch without breaking compatibility so it really isn't much better - yes it's got better tools, but nothing's going to make you use them unless your compiler has some clever analysis features you can turn on and do things like make raw "new" an error.
For me modern C++ is interesting, but it has even more of the problems that Rust has. There are lots of good ideas that help fix problems we've seen over the last 40 years or so (man, I'm old...), but I think unless you are already a C++ programmer it's probably more convenient to start with a language that was designed from the beginning to solve those problems. If someone paid me, I'd very happily write C++ code again, though.
Ha ha! Before I answer, I should say how much I appreciate the work you do. It really is awesome.
I've been a bit clumsy with my words, though. It's not so much that I know there is a more elegant way to do it as much as I'm left with that feeling. For example the ? operator. It's incredibly useful and makes the code much less clumsy. But I'm left thinking, "Why do I need this"?
I've got a list of actions, some of which could fail and some of which can not fail. I want to exit early if something fails. Is there another way I could represent this? For example, something like a "do" notation. Then each statement essentially needs to be a monad. I'm not necessarily keen on that, but I think we're missing an abstraction somewhere. The set of functions that all return the same type, describe a monad... Why am I mixing functions with different return types, I wonder...
The thing is, I've got this extra notation that I have to learn about that does something useful. It just appears to me that I wouldn't need that extra notation if I could abstract the execution differently. Maybe that's a pipe dream, but it gnaws at me :-) And, of course, if we had it, the result wouldn't be Rust -- you'd need a completely different language to support that way of looking at it. Just to get rid of ?. Yeah, not worth it for Rust, but I think still worth thinking about.
Sorry for the rambling nature of this post :-) Just to leave you with a more concrete example that's not about Rust. I actually like many things about Javascript. One of the things I found really elegant about JS is that there are no private or public variables. If you want something to be private, you just make a closure. There is no need for a "private" keyword or a "public" keyword. There is no need to build in special code in the interpreter. It just works because that's the way it works.
Another thing I found really elegant about JS (though, unfortunately not executed as far as they might have) is the way classes are represented. You write a function that returns a dictionary/struct. That's a constructor. But it's also pretty much everything you need in a class. A class is just a function. Although it's a bit bizarre, functions are also dictionary/structs, so if you need class functions, just add it to the function. I'm not so keen on how they implemented prototypical inheritance, but it was the 90's so I'll cut them some slack ;-)
So when I say "elegant", what I mean is that the form of the language dictates its function. With Rust, I find that we have quite a lot of stuff like the ? operator. It's super useful and leads to better code, but it's also kind of tacked on. If I were going to design a language that had similar utility, but didn't need the operator, what would it look like? Maybe there is no more elegant solution. However I kind of think that with another 5 or 10 years of work with Rust people will start seeing the kinds of workflow that Rust engenders and will probably discover abstractions that fits that workflow better -- much in the same way that Rust has done with C++.
It happens, it's just the internet. I have enough karma, haha.
> Before I answer, I should say how much I appreciate the work you do. It really is awesome.
Thanks! It's cool, I was just legitimately wondering. As you said sorta, it's helpful for me to understand people's perspective. If you'd like, I can give you some explanations here, but I wasn't asking to try and correct you, I was interested in your perspective. Thanks for sharing it :)
> “[no other] viable high performance language with the same ecosystem, support and tooling”
This comment strikes me as deeply contradictory. I have qualms with C++ as any other C++ programmer, but saying it’s famous for being famous is just ridiculous.
C++ has been used to operate nuclear submarines, airplanes, pacemakers, financial systems, ... over the last few decades. I’d like to know what you consider “battle tested”.
The hardest to maintain code is almost usually a mess of inheritance and generics/templates by someone far to "clever" that thinks OOP is the goal and not the tool. More often than not an inheritance hierarchy is basically (not literally) an abstraction over if/switch statements, except the logic will usually be spread out over multiple files and it's impossible to track all these potential branches in your head because they're purposefully obscured from you.
The OO style of programming also often moves the decision point. Instead of having an if statement above the 2 functions, you have the if statement at the construction of the object. If you happen to be writing code where you are doing mostly the same things, but certain details change in certain situations, you can group the differences into objects. If you are familiar with pattern lingo, this often happens if you are doing something that lends itself well to a template pattern. You can then use a bridge pattern to ensure that there is only ever 1 branch, instead of adding a decision point every time you want to vary the behaviour.
Where I find people (reasonably, IMHO) disagree is that having the decision point at the place where the behaviour diverges allows you to see the logic of the situation right at that point. If you have a bug in the code related to that, then it's very easy to figure out where it is, because all of the logic is local.
On the other hand, if you are refactoring the code, or changing the behaviour across the system, then it is very difficult to change because you have to search for all of those decision points. It's also harder to reason about on an architectural scale because it takes discipline (that may be lacking) to ensure that you always do things exactly the same way. You have a lot more flexibility because you can add all sorts of different logic to each decision point. The lack of constraint means that you have to inspect each case to determine if your overall change is going to work.
I'll be honest. I like OO. There are a lot of really diabolical ways you can write bad OO code, but there are lots of diabolical ways you can write any code. I'm familiar and comfortable with imperative, modular code. I'm familiar and comfortable with FP. I'm even familiar and comfortable with declarative code (I wrote some large XSLT systems). There are trade offs which every approach. It's easy to see problems in code that you are not happy with and then throw the baby out with the bath water. Having said that, I don't think there is a problem with having a preference of one style over another, but it's a good idea not to fool oneself into thinking that it solves every problem.
The very best programmers optimize their code for readability. Most of our day is spent reading code.
More often than not I find this isn't the case. If we have an inheritance tree of (A -> B -> C) then you'll run into cases where the bug is in a object of type C but it's calling a method defined on type A, overridden in type C and accessing a variable in type B. So you have to look at why A and C diverged, where the value is set in B, what else calls the method on A, etc. You have to know the whole graph and the abstractions are trying to hide it from you.
This is particularly true the older code gets because all those little "just overload this function" adds up and it can't be refactored because it's impossible to understand.
The future is neither pure OOP, nor pure FP, it is multi-paradigm.
And with nice libraries, IDE and GUI tooling as well.
Comparing languages based purely on grammar and semantics doesn't help much.
The declarative simplicity of FP makes it a joy, but I find myself skeptical that flattening the OOP hierarchy into what would be a right jolly long list of pure functions will prove practical.
Common Lisp ones make use of CLOS or similar concepts.
For Haskell you have Leksah, but it uses Gtk and Typeclasses which allow for polymorphism, a basic OOP principle.
In other words, I don't do hype or fads anymore.
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.
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.
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.
is not different from 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:
And the same with my example: In both cases the old `factorial` works and the enhanced one fails.Does
tells you that factorial definition changed and that it's dependent on prev? No.Does
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.
What should I be programming with?
Often people go the other way around and write entity relation diagrams. They write classes to encode the data that they want to work with and put the functions that interact with the data into those classes. This actually leads to code that isn't OO (again IMHO). It actually more closely resembles modular programming. You have areas where you say, "I'm dealing with that data here". All of your instance variables are essentially global to that module. Especially when the classes become large, this becomes mostly indistinguishable from imperative code with global variables.
Instead, consider the parameters that the methods on your class use. Imagine that you had no instance variables and that you had to pass all of the parameters to your functions. Now group all of the functions that use mostly the same parameters. That's a class. The instance variables represent some state of the program and the methods work on that state. Afterwards you can make diagrams to see how these objects interact with each other, but it usually doesn't make sense to do it before hand (because otherwise you end up with bags of data and functions that act on that data as if they were global variables).
Or, more concisely: An class is not a table in a DB and you shouldn't model it the same way ;-)
Btw. no, this is not how OOP works. With the method I outlayed I have very easy time to achieve idempotency, the thing you suggested makes it harder because the objects have internal state and the method calls can depend on those.
As for what should start with, it doesn't matter in the least. Start with whatever you feel like, or whatever your friends are doing, or whatever is convenient for you. Keep an open mind and have the attitude, "Well, I'm going to do it all sometime so I don't need what I'm doing now to be the 'best'". Don't try to save time -- you can't do it, because there is no end point in learning that you can achieve in your lifetime. Just progress down the paths that interest you and try to enjoy yourself.
In my experience, business applications are all about state. The state of an account, a customer contact, an invoice. Changing state is what messages / methods do. In a well designed and implemented system minimal inheritance gets the job done. And bypassing encapsulation is a big no-no. Polymorphism reduces cognitive load. A toString method should return a meaningful representation of the state of all objects in a well designed system, etc.
But fear not, the majority of banks, insurance companies, big business still use COBOL on mainframes. No danger of OOP there. It's all 1960s & 1970s programming with GOTOs and spaghetti code. And yes, those are the systems that manage trillions of "REAL" dollars everyday.
Some of the most literate, easy to comprehend and maintain, stateful, Gui-heavy desktop apps I've written / worked on have been detailed models of interactive objects, because it was appropriate for the problem.
In other problems (the classic web, data processing), it doesn't make as much sense to hydrate this huge, complex ecosystem of objects over and over just to throw it away again, or to end up with functions which are nonsensically bound to what is really just a data structure. Other operations are particularly recursive or parallel by nature and lend themselves to FP at least in part because of the work done in the FP community to handle those paradigms extremely well.
Surely some programming paradigms are objectively worse than others for certain use cases? I've personally come to the realization that OOP is only useful in very niche cases.
I don't think OOP as we see it in most languages today is particularly helpful. I do increasingly feel that data structures and operations should not be muddled together, and the polymorphism strategy should be something more like Haskell's typeclasses/Rust's traits or Go's idea of interfaces.
I'm also sure that using those for a few decades would show us plenty of things wrong, and lead to something else.
We are still children in this world. There are no correct answers - but there are definitely some wrong ones, and the increasing realisation of that is evident in the popularity of languages like Go and Rust, and the development of C# is also interesting - increasingly new language features are FP features, not OOP features, and the language becomes increasingly hybrid. It will always be limited by being built on a fundamentally OO runtime, but it's interesting how things are changing.
Similarly, encapsulation (bundling code and data) definitely has its uses - in preserving class-wide invariants, or in providing a "common" interface that makes it possible to disregard the inner workings of some data structure, and interact with it in a way that's not dependent on the implementation. But in practice, implementation inheritance in OOP often encourages what amounts to breaking encapsulation and making it possible to violate class invariants, all in search of some dubious "code reuse". If you can't express your desired code reuse pattern via some combination of simple interface inheritance, delegation and dispatch over "sealed", non-extensible variants (the mechanisms that plain old composition/FP gives you) there are probably some underlying problems with it that you haven't thought through.
Of course the standards of programming improve continuously over time. Even FP and broadly "FP-like" programming is not what it was in the 1980s, and some of that progress was brought (if perhaps incidentally!) by OOP languages. This is to be expected!
Enough said
The same thought expressed with enough meat to chew on might provide a worthwhile addition to the conversation but such venears once pierced are often as solid as movie sets.
Think more deeply and take time to share.
Its like this guy has never used an OO language or at least tried to use some FP paradigms in one.
I don't understand, I actually though medium was sort of free (as in beer), who is getting this money?
Really depends on what is your definition of "OOP" here
> Both Smalltalk and Erlang make use of OOP the way it was originally intended by its inventor, Alan Kay.
ah, the bad definition of OOP then.
Hint : most C apis that look like
are OOP, not procedural. They just lack the syntax sugar.> Linus Torvalds is widely known for his open criticism of C++ and OOP.
yet he went on doing C++ for user-space code : https://www.youtube.com/watch?v=ON0A1dsQOV0
> Some might disagree with me, but the truth is that modern OOP has never been properly designed. It never came out of a proper research institution
as someone who did a phd in a "proper research institution", that's a completely bullshit criterion.
> OOP doesn’t have decades of rigorous scientific research to back it up
oh come on
https://scholar.google.com/scholar?q=object-oriented
ADT's/"Object-based" programming (which is what you show above) is not the issue. The biggest problems occur when you introduce non-trivial inheritance/polymorphism.
I smell a no-true-scotsman incoming..
>> Both Smalltalk and Erlang make use of OOP the way it was originally intended by its inventor, Alan Kay.
> ah, the bad definition of OOP then.
Message-based OOP is usually considered to be superior to inheritance by even OOP advocates, and even composition-based OOP by many. I don't see your evidence for "bad definition of OOP".
>Hint : most C apis that look like
> foo* foo_create(void);
> int foo_get_bananas(foo);
> void foo_print(foo, int, int);
> void foo_free(foo);
>
> are OOP, not procedural. They just lack the syntax sugar.
Not every API that uses a struct is OOP. If your foos use inheritance, virtual dispatch etc, then yes it's OOP. But usually it's just something like a struct to store state instead of having globals or multiple parameters even.
>> Linus Torvalds is widely known for his open criticism of C++ and OOP.
> yet he went on doing C++ for user-space code : https://www.youtube.com/watch?v=ON0A1dsQOV0
He's a pragmatist first and foremost, and the GUI libraries he uses (Qt) and the programmers that work on GUI code use C++, he said. I doubt he would write his own GUI framework in C++.
"THE codebase becomes impossible to maintain" "THE codebase becomes too complex and unmaintainable" "What makes THE codebase complex?"
I count fifteen references to "the codebase." THE codebase. THE.
Well your program consists of more than one codebase! What then?
Here's some code that ran on the linked site:
Medium (codebase A) dynamically interrogates the API provided by my browser (codebase B) as influenced by plugins (codebase C) linking the UI framework of my OS (codebase D) and then loads some polyfills provided by Babel (codebase E)... Which one is "the codebase?"Your runtime environment is multiple components, each evolving independently! How do you write reliable software here? You can use static types but can't trust them, they can't predict the future and don't know the past. You want runtime interrogation and reflection. You want graceful degradation and dynamic polyfills. You need late bound interfaces. You need objects, not ADTs.
This I think is the heart of the OOP idea: software components realized in classes/objects/messages which can evolve truly independently. Drop the "THE codebase" assumption and some of OOP's strengths will appear.
If that is what you are saying, that is nonsense.
No it's not. It's a crown jewel of programming.
What do proper CS-researchers have to do to not be grouped together with pixel pushers? Slap Theoretical on every paper?
BTW: Are you considering Go to be OOP? Like it always is in the programming world everything is ill defined. Go has no inheritance from what I've read.
Source lines of code is a proxy to the complexity of the problem in hand. With the assumption that the engineers will not unnecessarily write code just to fill up the code base or pass time, we can safely say that the number of lines of code for a database will be significantly higher than say a networking library. To write more lines of code, we need more engineers. And with more engineers, we uncover a variety of problems.
No accomplished Haskell programmer will agree that Go is a functional language. At least the ones that I know don't. I think FP programmers are not in agreement as to what constitutes a functional language, example Haskell vs Clojure vs Elixir.
My point today is that, if we wish to count lines of code, we should not regard them as "lines produced" but as "lines spent": the current conventional wisdom is so foolish as to book that count on the wrong side of the ledger. - Dijkstra
The required techniques of effective reasoning are pretty formal, but as long as programming is done by people that don't master them, the software crisis will remain with us and will be considered an incurable disease. And you know what incurable diseases do: they invite the quacks and charlatans in, who in this case take the form of Software Engineering gurus. - Dijkstra
Do you have anything original to add?
Definition of OOP has changed over the years, early on it was a lot of focus on inheritance & polymorphism, but today it is mostly about composition, which is basically what you do in FP as well.
New inventive ideas like Go's & Typescript's interfaces flips the relationship between the caller & the callee. Or Rust's traits where you can expand your existing types with new behavior.
There is criticism about OOP to be said, but using Java as your strawman to invalidate OOP is not really a good argument, it is just one implementation of many.
Edit: removed the word dynamically from description about Rust traits.
Java has added more & more FP style constructs, does it make Java a FP language?
Rust is not referentially transparent, does not model effects (any function can rm -Rf your drive), permits shared mutable state (RefCell, etc) includes statements not just expressions (break, return). Its most important safeguard is its affine type system, which is absent from Haskell.
Item declarations, let statements, expression statements, and macro invocation statements are the four kinds of statements in Rust.
> Real-world example, please!
> Real-world example, please!
Would it be too much to ask for a REAL real-world example? Unless you are already an expert in both paradigms you (I) have no idea what is the author talking about.
How is nullability supuerir in Functional?
How is error handling superir in Functional?
What is really meant by shared mutable state? (The example with a piece of papaer is equally useless as the explanation that "state is any temporary data stored in memory")
How are "erroneous abstractions" promoted by OOP?
What is the "low signal-to-noise ratio"
How is the state NOT promiscously scatered between objects in Functional programming?
How do you make "core parts of your application be stateless and pure" in Functional programming?
How is global state "the root of all evil"?
Could someone point me to a literature that can explains these questions to someone who don't know the answers already?
Particularly the chapter "Relational Databases" which compares to programming with sets in databases. This book doesn't tell you to program as if you worked with SQL, but draws inspiration from it and makes a comparison.
15 years later, I am now being told to dump my decade and a half knowledge and competitive advantage and learn a new programming paradigm? Miss me on this one. This time around, I will stick to what I know best. OOP has 80% of what I need and I know ways to deal with 20% of the problems. I will stick to it.
It took me only 2-3 days to learn F# but I feel I will be doing my clients a disservice if I have to start from scratch and start delivering solutions to their problems using a programming paradigm that I have little experience with.
No. Thank you. I will stick to delivering solutions to my clients using what I know best.
I think as developers we should constantly learn about new approaches. Doesn't mean we need to use them blindly and follow the herd. Sometimes it's also useful to learn something to realize why one should not use it, and how the current approach is actually better.
At age 37, I feel young and really wish that I will still be able to program at age of 50 and beyond.
I've heard good things about F# for business logic, but haven't used it myself yet.
`The Pure Function Pipeline Data Flow`, based on the philosophy of `Taoism` and `the Great Unification Theory`, In the computer field, for the first time, it was realized that the unification of hardware engineering and software engineering on the logical model. It has been extended from `Lisp language-level code and data unification` to `system engineering-level software and hardware unification`.
Whether it is the appearance of the code or the runtime mechanism, it is highly consistent with the integrated circuit system.
Using the input and output characteristics of pure functions, pure functions are used as pipelines. Dataflow is formed by a series of pure functions in series. A dataflow code block as a function, equivalent to an integrated circuit element (or board)。 A complete integrated system is formed by serial or parallel dataflow.
It has also been widely unified with other disciplines (such as management, large industrial assembly lines, water conservancy projects, power engineering, etc.). It's also very simple and clear, and the support for concurrency, parallelism, and distribution is simple and natural.
There are only five basic components:
1. Pipeline (pure function)
2. Branch
3. Reflow (feedback, whirlpool, recursion)
4. Shunt (concurrent, parallel)
5. Confluence.
The whole system consists of five basic components. It perfectly achieves unity and simplicity. It must be the ultimate programming methodology.
This method has been applied to 100,000 lines of code-level pure Clojure project, which can prove the practicability of this method.
[The Pure Function Pipeline Data Flow](https://github.com/linpengcheng/PurefunctionPipelineDataflow)
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."""
X is terrible, Y is super! Fact is that everything has benefits and drawbacks, and so does OOP, Functional programming or whatever.
The strength about OOP is that it's practically the best to program most software. If you look for example at the SDL library written in C, it uses an OOP interface with handles to keep things nice and simple. Yeah that's right, OOP in C, why would anyone do that? Because it has benefits, that's why.
So why is most software nowadays written in OOP? Because it has the most benefits. You can sure find examples where functional programming would be better, but they are a minority.
So yeah, next time I would expect an article to properly make a comparison between benefits and drawbacks of both things that are compared. Not the usual "all drawbacks of X, but look at all benefits of Y". This kind of thing screams "Junior alert" to me.
Meantime, I'm developing my apps and games and applications in OOP, and absolutely loving it. Wouldn't trade it for anything else in the world.
Sure the article isn't super well-written, but it raises a fair point and shouldn't just be thrown away. Just because something is the most used or standard does not mean it is the best solution. Eating junk food is very common, but you would hardly argue that it is the best way for a human to get nutrients.
You can use OOP all you want, and it's fair to say that the article should have been less biased (though he has a disclaimer that says he is biased), but there is nothing suggesting that OOP is practically the best paradigm to program most software other than the fact that it is the most used.
Maybe we are talking next to each other about what a good programming language is. For me as a professional programmer, it means to sell value. This implies that the companies I work for can make the most amount of revenue. Thanks to free market forces, we can assume that the most "optimal" programming languages survive. Remark that "optimal" is not expressed in terms of aesthetics or whatever, but is purely the productivity of generating money.
You can indeed claim that fast food is not healthy, but from a business perspective, it is a success story, and therefore also popular. I don't know how "healthy" OOP is, but it is hard to argue that it is not a business success story. And that is exactly what I offer to my clients.
Edit: And to be honest, I would have no clue how I could make the business case for any of the projects I ever worked on for not using OOP. These include games, consumer mobile apps, robotics and various business applications (smart client-server, etc).
Maybe we are talking next to each other about what a good programming language is.
Yes, I think we are at least in a sense. If we are speaking about programming languages from a business perspective we can agree that the language leading to the most productivity will - in a business sense - be the most optimal. I think my point is we have been productive in spite of OOP and not because of it. OOP here meaning the way it has been since Java and not Alan Kay's original ideas. When something like OOP becomes ingrained in an industry like it has been in software development in both the business and in academia, I'm not sure just waiting on the free market is going to do us any good if we (i.e. me) want to change it. Personally I am vastly more productive in the non-OOP code-bases I have worked in as opposed to the opposite. This is a personal bias, but I am no the only one and there is a growing movement of moving away from it and often already completely gone away from it. So it's not just a case of the grass being greener.
I assume you are more senior than me, since I am not particularly senior, so you can again write it off as me being a junior. But it's a growing paradigm shift, especially in game and game engine development, which is what I do for a living.
You got me intrigued about being less productive in OOP. Can you share what exactly you are working on? My first guess would be that it has something to do with concurrency or parallelism? It especially interests me since it's game development :).
I see this in my current senior colleagues, who may be very OOP-oriented but they are incredibly productive because they are super experienced in their field and in the code-base. I 100% respect that type of seniority of course, because I have learned a lot from my seniors.
Regarding my productivity I think it is first and foremost related to how my brain solves problems. I especially like the idea of Data-Oriented Design, because I think of solving programming problems as three simple steps: 1. Some data comes in 2. Data needs to be transformed into something else (based on some business rules) 3. Data needs to be presented
So when I want to solve a problem I first define my data, then I define my transformation functions and then where they are used afterwards. It's not necessarily a purely functional approach, since they do change state as opposed to returning new state, but it has very little overhead in my brain. And as you mention concurrency, it simplifies it vastly because you usually know exactly who is using which data where.
I work mostly on engine development, so there's a lot of room for optimization and understanding how a lot of data is transformed every frame, which makes performance a super interesting topic for me. I can reason much more easily about performance in a procedural code-base than in an OOP code-base, so that might be part of my bias. :)
"Out with systems; in with formal languages!"
https://www.dreamsongs.com/Files/Incommensurability.pdf
> I do not consider Xerox or another enterprise to be a “proper research institution”.
"Also, I don't consider a paper on systems to be publishable!"