OOP is not that bad, actually
The article compares Object-Oriented Programming (OOP) and Haskell's functional programming, highlighting OOP's advantages in collaboration, library extension, and backward compatibility, while noting Haskell's challenges in these areas.
Read original articleThe article discusses the merits of Object-Oriented Programming (OOP), particularly in statically-typed languages, and contrasts it with functional programming approaches, specifically Haskell. The author acknowledges that while OOP may not be their preferred paradigm, it offers significant advantages for collaborative programming over extended periods. Key features of OOP, such as classes, inheritance, subtyping, and virtual calls, facilitate the development of composable libraries and allow for backward-compatible extensions. The author illustrates this with a logging and database example, demonstrating how OOP enables the addition of new functionalities without altering existing code. In contrast, the author explores how similar functionality could be achieved in Haskell, highlighting the challenges of maintaining backward compatibility and the complexity introduced by type parameters and effect monads. The article concludes that while Haskell offers powerful abstractions, OOP's straightforwardness in evolving libraries without breaking existing code is a notable advantage.
- OOP provides a structured approach to programming that supports collaboration and long-term maintenance.
- Key OOP features allow for backward-compatible library extensions without requiring changes to existing code.
- Haskell's functional programming model presents challenges in achieving similar flexibility and backward compatibility.
- The article emphasizes the trade-offs between OOP and functional programming paradigms in practical software development.
Related
I Probably Hate Writing Code in Your Favorite Language
The author critiques popular programming languages like Python and Java, favoring Elixir and Haskell for immutability and functional programming benefits. They emphasize personal language preferences for hobby projects, not sparking conflict.
When Objects Are Not Enough
Tony Messias examines Object-Oriented Programming's foundations, emphasizing actions in Laravel, historical context from Smalltalk, and critiques modern interpretations, advocating for a deeper understanding of OOP principles and experimentation.
Objective-C is like Jimi Hendrix (2014)
The author compares Jimi Hendrix's influence on guitar music to Objective-C's impact on programming, noting how perceptions of its features have evolved as newer languages emerged, reflecting on programming history.
Objective-C is just, like, a leaky abstraction over C
Objective-C is described as a "leaky abstraction" over C, integrating modern programming features and enabling interoperability. Its design is effective, with Objective-S proposed as a streamlined variant.
Objective-C is just, like, a leaky abstraction over C
Objective-C is described as a "leaky abstraction" over C, integrating modern programming features and enabling interoperability. Its design is effective, with Objective-S proposed as a streamlined variant.
The simple truth however is that overly going into either functional or OOP camp will hurt because strict adherence becomes subscribing to a silver-bullet.
The middle road is simply a better engineering option, use a practical language that supports both paradigms.
Keep data transforms and algorithmic calculations in functional style because those tend to become hot messes if you rely overly on mutation (even if there is performance gains, correctness is far far easier to get right and write tests for with a functional approach), then there are other concerns where an OOP derived system with inheritance abstractions will make things easier.
Several patterns are trivial, others are very similar and are just a linked list of objects that are searched for performing some action.
The composition over inheritance meme in the book does not make things easier (there is no "composition" going on anyway, it is just delegation).
Objects themselves for resource cleanup like RAII are fine of course.
Logger l;
That’s a Logger, and it will not magically become a subclass.And changing this to go through pointers everywhere and to use virtual functions, in C++, is not very performant. A good JIT compiler may be able to effectively devirtualize it, but C++ compilers are unlikely to be able to do this effectively.
The problem domain is things like your physical model. Your 3d mesh. Financial transactions. All things that need to be represented in software.
The software domain is things like components and queries. Things you can build in-software to realize your problem domain on a computer.
Defining a component and the behaviors of a component has nothing at all to do with the problem domain; it's purely an artifact of software to make your system more extensible/reliable/faster/whatever. But this is the part that's OOP excels at because you get to write the rules of your component taxonomy in its entirety. You have a ThingDoer class that can accept any number of Components to give it the behaviors you need, and then you start writing out components to model your problem domain. The objects and their behaviors remain at the level of "how do I put these pieces together". OOP sucks for the problem domain, though, because you're always stuck trying to munge some limited inheritance tree (animal <- dog <- wolf) onto the Real World and it's always going to miss something. Far better to build atomic building blocks that are expressive enough to compose into whatever you need.
Here's the first article in the series: https://ericlippert.com/2015/04/27/wizards-and-warriors-part...
It's especially odd to compare and contrast OOP style with Functional Programming paradigm because these things are orthogonal.
The following might sound ridiculous, but I swear I'm not making them up:
- In my highschool, students on their "Computer & Information 101" class were asked to answer what polymorphism is. Most of the said students had zero programming experienece at the time.
- In my sophomore year (CS major), students were asked to finish a mini game "with design patterns" and explain what design patterns they used. For most of the said students, that was the first time they wrote a program with more than 300 LoC. Before that, all the assignments they had seen are "leetcode-like", like implementing Sieve of Eratosthenes in C.
> I think it would be beneficial for the functional programming community to stop dismissing OOP’s successes in the industry as an accident of history and try to understand what OOP does well.
But the author has spent enough time in the haskell ecosystem, and probably has some cause for this statement. I would personally have liked to hear more about that cause, and the perceived issues in the community, rather than code examples.
But in real life, when there is a team, you run into the fragile base class [1] constantly and changing that base class causes horrible issues across your code base.
I have found that OOP with inheritance is actually a form of tight coupling and that it is best to not use class hierarchies.
I agree with encapsulation and modularity and well defined interfaces (typed function signatures are amazing.) I just completely disagree with inheritance in all forms.
There are no benefits to it (besides feeling smart because you've made an elegant but ultimately brittle ontology of objects and methods), just a ton of downsides.
[1] https://en.wikipedia.org/wiki/Fragile_base_class#:~:text=The....
OOP certainly has good features (e.g. encapsulation of state), but I think it tends to shine best when programmers are really aware of the trade-offs. Most aren't. The same person that agrees that mixins are a bad idea in React, will then turn around and happily organize their logic as class-based views in Django.
And due to sunk cost, it's nearly impossible to convince someone who's invested time in this paradigm that the acrobatics are often probably unnecessary.
In my opinion, newer languages expose programmers to better mental models than "the class hierarchy" to solve code organizational problems. Work with Go or Elixir for a while and see your Java and Python improve.
> pandas.read_csv(filepath_or_buffer, *, sep=<no_default>, delimiter=None, header='infer', names=<no_default>, index_col=None, usecols=None, dtype=None, engine=None, converters=None, true_values=None, false_values=None, skipinitialspace=False, skiprows=None, skipfooter=0, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=<no_default>, skip_blank_lines=True, parse_dates=None, infer_datetime_format=<no_default>, keep_date_col=<no_default>, date_parser=<no_default>, date_format=None, dayfirst=False, cache_dates=True, iterator=False, chunksize=None, compression='infer', thousands=None, decimal='.', lineterminator=None, quotechar='"', quoting=0, doublequote=True, escapechar=None, comment=None, encoding=None, encoding_errors='strict', dialect=None, on_bad_lines='error', delim_whitespace=<no_default>, low_memory=True, memory_map=False, float_precision=None, storage_options=None, dtype_backend=<no_default>)
An OOP approach would define a Reader object that has many methods supporting various configuration options (setSkipRows(..), setNaFilter(...), etc), perhaps using a fluent style. Finally you call a read() method that returns the DataFrame.
This is odd to me. There are solutions to make data types extensible in haskell, but for example in purescript you'd just use a record. But more importantly, why would you want existing functions to use the new type? Why not just pass the _logger from FileLogger in? The existing functions can't use the _flush ability anyway (in both OOP and FP cases)
On r/haskell, user mutantmell gave a implementation of the code (assuming such a module system) closely following the Dart code given in the original post.
https://gist.github.com/mutantmell/c3e53c27b7645a9abad7ef132...
https://www.reddit.com/r/haskell/comments/1fzy3fa/oop_is_not...
https://www.reddit.com/r/haskell/comments/1fzy3fa/oop_is_not...
>(Java-style) OOP offers a solution for this: you can code against the abstract type for code that doesn't care about the particular instance, and you can use instance-specific methods for code that does. Importantly, you can mix and match this single instance of the datatype in both cases, which I believe to be a superior coding experience than having two separate datatypes used in separate parts of your code.
>"Proper" module systems (ocaml, backpack, etc) offer a better solution that either of these: when you write a module that depends on a signature, you can only use things provided by that signature. When you import that module (and therefore provide a concrete instance of the signature), the types become fully specified and you can freely mix (Logger -> Logger) and (SpecificLogger -> SpecificLogger) functions. This has the advantage of working very well with immutable, strongly-typed functional code, unlike the OOP solutions.
>This is in essence the same argument for row-polymorphism, just for modules rather than records. It can be better to code against abstract structure in part of your code, and a particular concrete instance that adheres to that structure in other parts.
I am confused by this statement or it is going against what I understand.
If you have created a Type B variable, and you also have a new interface called A, and B implements A, then why would Type B variable's values be passed to A's values. 'A' is only an interface.
"It is not difficult to keep a functional aspect in OO."
"It is not that difficult to do OO in Pascal or similar" (quote from Wirth)
Several functional programming languages includes an OO or OO like feature.
Nothing will fit every situation better than anything else.
OP's example is trivially solved by services in both, because both treat dependencies as first class constructs and types.
PS: I really hate python style paradigm & declarative programmings. Rust is top of my ignore list.
And I feel like code culture is one place in need of some checks, on it's checking. There's so many wide-ranging beers out there, prejudices. Some have fought those battles & have real experience, speak from the heart. But I feel like over time the tribalisms that form, of passed down old biases, are usually more successful & better magnets when they are anti- a thing than pro a thing.
JavaScript, PHP, Ruby, rust. Systemd, PipeWire, Wayland. Kubernetes. OOP, CORBA, SOAP. These are examples topics are all magnets for very strong disdain, that in various circles are accepted as bad.
It's usually pretty easy to identify the darksiders. Theres almost never any principle of charity; they rarely see in greys, rarely even substantiate or enumerate their complaints at all. I've been struggling to find words, good words, for the disdain which doesn't justify itself, which accepts it's own premise, but the callous disregard & trampling over a topic is something I would like very much to be a faux pas. Say what you mean, clearly, with arguments. Manage your emotional reactions. Don't try to stir up antagonism. If you can, cultivate within yourself a sense of possibility & appreciation, even if only for what might be. Principles of Charity. https://en.wikipedia.org/wiki/Principle_of_charity
I'm forgetting what else to link, but around the aughts this anti- anti-social behavior has a bit of a boom. Two examples, https://marco.org/2008/05/21/jeff-atwood-who-knows-nothing-a... https://steveklabnik.com/writing/matz-is-nice-so-we-are-nice...
(And those on the pro side need to also have charity too.)
The idea of the Speaker For The Dead, someone who tries to paint clearly both upsides and downsides of a thing, is one I respect a lot & want to see. A thing I wish we saw more of.
(I feel like I have a decent ability to see up and down sides to a lot of the techs I listed. One I'd like better illumination on, a speaker for the dead on: CORBA.)
Inheritance's problem is not that it is "intrinsically" bad, but that it is too big. It is the primary tool for "code reuse" in an inheritance-based language, and it is also the primary tool for "enforcing interfaces" in an inheritance-based language.
However, these two things have no business being bound together like that. Not only do I quite often just want one but not the other, a criticism far more potent than the size of the text in this post making it indicates (this is a huge problem), the binding introduces its own brand new problem, the Liskov Substitution Principle, which in a nutshell is that any subclass must be able to be be fully substituted into any place where the superclass appears and not only "function correctly" but continue to maintain all properties of the superclass. This turns out to be vastly more limiting than most OO programmers realize, and they break it quite casually. And this is unfortunately one of those pernicious errors that doesn't immediately crash the program and blow up, but corrodes not only the code base, but the architecture as you scale up. The architecture tends to develop such that it creates situations where LSP violations are forced. A simple example would be that you need to provide some instance of a deeply-inherited class in order to do some operation, but you need that functionality in a context that can not provide all the promises necessary to have an LSP-compliant class. As a simple example of that, imagine the class requires having some logging functionality but you can't provide it for some reason, but you have to jam it in anyhow.
It is far better to uncouple these two things. Use interfaces/traits/whatever your language calls them that anything can conform to, and use functions for code reuse. Become comfortable with the idea that you may have to provide a "default method" implementation that other implementers may have to explicitly pick up once per data type rather than get "automatically" through a subclass inheritance. In my experience this turns out to happen a lot less than you'd think anyhow, but still, in general, I really suggest being comfortable with the idea that you can provide a lot of functionality through functions and composed objects and don't strain to save users of that code one line of invocation or whatever.
Plus, getting rid of inheritance gets rid of the LSP, which turns out to be a really good thing since almost nobody is thinking about it or honoring it anyhow. I don't mean that as a criticism against programmers, either; it's honestly a rather twitchy principle in real life and in my opinion ignoring it is generally the right answer anyhow, for most people most of the time. But that becomes problematic when you're working in a language that technically, secretly, without most people realizing it, actually requires it for scaling up.
So how could we compare OOP to FP in a way that evens out this difference? It depends on how you define FP.
You can (like this article seem to) restrict the definition of FP to only purely functional programming, in which a program cannot directly execute side effects, and must return a value representing the effects that the runtime system will then execute. Then an apples-to-apples comparison would compare the FP program with an OOP program that uses an effects system to manage its effects.
How do we do that? Well, if we define FP in a way that forces us to use effects, then the definition excludes a language like Scala, which is essentially a side-effecting OO imperative language that has features that enable FP-style programming. Scala isn't FP by the article's definition, because you can write impure code, but it does let you write programs that manage effects using an effects system. So you can do a reasonably fair comparison that way. I think you would discover that the pain of using effects is the same, if not greater, in an OO language where they have to be added as a framework.
Or you could define FP more broadly to include side-effecting languages like Clojure and F#, and you could compare a side-effecting OO program to a side-effecting FP program. This would be tricky because it would be very difficult to draw a style line between OOP and FP. Would you allow the FP program use OO constructs and the OO program to use FP constructs? If so, you might end up comparing two identical Scala programs. Would you ban the FP program from using OO constructs and ban the OO program from using FP constructs? In that case, you would get an OO program in the style of the 1990s or 2000s, which wouldn't be fair to modern OOP.
I don't think either choice really leads to a meaningful comparison between OOP and FP. I think comparisons have to be more specific to be meaningful, and they have to be in the context of a particular application, so you can fairly compare programs that use effects systems with ones that don't. You can compare Java with Haskell for a particular application. You can compare C# with F# for a particular application. You can compare Scala with an effects system like Cats Effect to Scala without an effects system, again for a particular application. These comparisons are more realistic because you can take into account the pros and cons of using an effects system versus not for the given application.
Related
I Probably Hate Writing Code in Your Favorite Language
The author critiques popular programming languages like Python and Java, favoring Elixir and Haskell for immutability and functional programming benefits. They emphasize personal language preferences for hobby projects, not sparking conflict.
When Objects Are Not Enough
Tony Messias examines Object-Oriented Programming's foundations, emphasizing actions in Laravel, historical context from Smalltalk, and critiques modern interpretations, advocating for a deeper understanding of OOP principles and experimentation.
Objective-C is like Jimi Hendrix (2014)
The author compares Jimi Hendrix's influence on guitar music to Objective-C's impact on programming, noting how perceptions of its features have evolved as newer languages emerged, reflecting on programming history.
Objective-C is just, like, a leaky abstraction over C
Objective-C is described as a "leaky abstraction" over C, integrating modern programming features and enabling interoperability. Its design is effective, with Objective-S proposed as a streamlined variant.
Objective-C is just, like, a leaky abstraction over C
Objective-C is described as a "leaky abstraction" over C, integrating modern programming features and enabling interoperability. Its design is effective, with Objective-S proposed as a streamlined variant.