When FP? And When OOP? (2013)
raganwald.com
raganwald.com
Type payment = Invoice(Address) | Card(CardDetails)
And then match/switch on those things, while the compiler will tell me if I did something wrong such as not handle one case. Adding a third type of payment (Cash) is as simple as adding "| Cash" to the definition - and the compiler will tell me exactly where I haven't handled cash payments.A simple smell test for a programming language is this: if describing the above takes more than the above code - your language has a wart. If you try to describe this properly in C# you'll be writing an abstract outer class and N concrete inner classes, plus boilerplate for comparisons and so on. You'll be looking at likely 50-100 lines of code where no single line of code actually shows whan what you are modeling. This is the terrible part of OO. I'm sure there are a few cases to show an FP weakness handled elegantly by OO - but I'm equally sure those cases are fewer.
Also, there is nothing particularly "FP" about ADT's. They are just common in FP languages. Any OO language with a sum type and exhaustive matching can do this. But typically, such as for Java, C++ and C# they don't (yet). There are languages that support this that aren't FP (but not necessarily OO either) such as Rust and Kotlin.
Pattern matching is the concept that they all have in common, that I miss when it isn't available. You don't have the 'exhaustiveness' check in Elixir because it's not possible in a dynamic language (dialyzer aside), but it's still a very powerful tool.
(match payment
[('invoice address ) (foo address )]
[('card card-details) (bar card-details)])
As you say, there's no exhaustiveness check; plus the language won't guarantee that the product types are adhered to. For example, someone might give us `('invoice card-details)` and the language wouldn't complain.Racket's contract system can help with this; although it's very slow.
This is the "expression problem" https://en.wikipedia.org/wiki/Expression_problem
In FP with ADTs, anyone can write new functions for a datatype. Yet, as you say, adding a new case requires modifying the definition and existing functions.
In OOP, anyone can add a new case, by defining a new subclass. Yet writing a new method for that class requires modifying the definition and existing subclasses.
One of these isn't always better than the other, so it's useful to have both.
> If you try to describe this properly in C# you'll be writing an abstract outer class and N concrete inner classes, plus boilerplate for comparisons and so on.
This overhead is due to emulating one approach using another. It would also take a bunch of boilerplate to implement subclassing with ADTs (e.g. a sum type to describe the methods, a smart constructor to implement the inheritance, etc.).
(Personally I prefer ADTs too, but they're not a magic bullet)
- All instances are thrown into one global namespace, so we don't need to worry about where to find the right one.
- There can be at most one instance per type. Hence there's no ambiguity about which one to pick.
These make it easy for the machine to pick the correct instance automatically. Unfortunately, it also means that every time we import or upgrade a library, there's the chance that it will break things by exposing an instance that clashes with what we have.
The "backpack" stuff looks interesting in this regard, although it seems very over-engineered (mostly due to legacy concerns).
It's the main thing I've missed from OCaml when programming in Haskell (you can sort of do something like this with typeclasses, but I much prefer the OCaml solution).
The same is true in java if you use instanceof. The thing is, just like enums inductive data type definitions are not supposed to be extended by any module other than the one declaring them, and adding more should be considered an API change.
Java isn't a particularly pure example of OOP: it has `if`/`for`/`while`/etc. from structured programming, non-OO "primitive types", non-polymorphic `+`/`-`/`*`/etc. and so on. `instanceof` is another example of a non-OOP feature (the OOP approach would be an `isFoo` method, where each class can decide what to return).
> inductive data type definitions are not supposed to be extended by any module other than the one declaring them, and adding more should be considered an API change.
Exactly. Yet in OOP, anyone can add new subclasses whenever they like, without ever telling the original author, and instances of those subclasses can co-exist alongside any other subclasses and will (in theory) work fine with all the existing code written for the superclasses.
What if I altered your sentence to say the following:
"Functions acting on a datatype are not supposed to be extended by any module other than the one declaring that type, and adding more should be considered an API change."
This would seem crazy if we applied it to an FP language; yet that's exactly the case in OOP, where a fixed set of methods are implemented inside each class.
"Extend program with new data types and new functions"
It is quite well studied and solved in various languages. And, in my opinion, typical OO language solution does not come as especially succinct.
I wouldn't say the expression problem is "solved"; various languages pick different tradeoffs, but the underlying problem is still there.
The best description I've seen of this is at https://www.tedinski.com/2018/02/27/the-expression-problem.h...
Combine that with the proposed new enum syntax and existing sealed trait plus case class/object boilerplate will be gone. Looks pretty great, but we'll have to wait until next year sometime to use it in production.
My feeling is that better database-to-app-language integration is needed. Databases better handle big-picture issues, but integrating them with code is a pain in most frameworks because app world and database world ended up too different for unknown reasons.
It seems there was an anti-database fad(s) starting in the mid-90's that made app languages want to pretend databases didn't exist, to be magically hidden behind an interface. We are paying the price for this head-in-the-sand thinking. We should find a way to embrace databases rather than try to wrap them away, because that failed.
This assertion entirely misses the point.
In most applications which employ a db to handle persistence there is a need to map relational data to objects. Either you reinvent the wheel each time you need to handle data to/from the persistence layer, by rolling your own ORM infrastructure, or you reuse a component that is far less bug-prone and efficient and far easier to maintain and update. Framework developers recognized the need to free developers from wasting time writing bug-ridden and ineffixient boilerplate code, and thus started offering their own ORM frameworks. Real-world developers who felt the pain of having to roll their own ORM components or recognized the value of using those tools, whether in reducing bug count, increased performance, lower time to market, and lower maintenance needs.
Your comment sounds an awful lot like the comments from decades ago where random geeybeards whined that these new fangled compilers and interpreters want to pretend machine code doesn't exist, and how hand-written assembly is the right tool for the job.
I'm still looking for that. Where is it? I've yet to find a good ORM. They end up being dark grey arcane boxes that require dedicated specialists to troubleshoot and tame. It appears objects and RDBMS just don't mix well: https://en.wikipedia.org/wiki/Object-relational_impedance_mi...
I suggest we look at using more relational concepts on the application side instead of trying to translate back and forth between paradigms, spending too much grey matter and code on translating and converting back and forth.
Oracle Forms shows some possibilities. It requires about 1/5 the source code of OOP frameworks for similar CRUD applications. There are clunky aspects to Oracle Forms, I admit, but I believe the good stuff can be borrowed without also being forced to take the bad stuff. It's a starting point to app/DB integration exploration.
In digital logic circuits simulation you will be forced to compute things transactionally - update all relevant outputs at once, - otherwise you will have interesting and subtle errors to debug.
For single clock domain designs it is best done using Haskell's data type like "data S a = S a (S a)", i.e., infinite lists where each element is a state of a "bus" at the beginning of the clock cycle. The register then can be defined as "register resetState inputs = S resetState inputs", addition as "addS (S x xs) (S y ys) = S (x+y) (addS xs ys)" and so on.
Then you can combine these "S-processors" in many interesting ways and throw Quickcheck at them with LTL specification of behavour (when input satisfies these conditions, then output must satisfy these) and, after investing some more time, even get working Verilog source for entire system.
And then the compiler will tell you all the stuff you have to do to implement a payment type. It might be a little or it might be a lot. Your ADT example also might be a little work or it might be a lot. I know some, or many, “frameworks” in the oop world might have that abstracted out n degrees for some rare or imaginary problem where you have to implement the abstractpaymetypefactoryfactory and all it’s constituents... to be fair that would ha e the ability to solve that rare or imaginary other problem though.
Traditional OO focused on extensibility over everything else. I think the idea of openness to extension is way oversold. The idea of a closed and simple to overview enumeration is much more common in my experience (after 25 years of OO...) and it’s sadly hell to create in C# compared to any language with ADTs.
Yes, I know types don't "have to" be hierarchical, but they are usually messy to manage as non-hierarchies.
For applications that will scale horizontally, such as web apps, I think FP makes more sense, since computation over data can spread across multiple servers/processors. Where as for applications that cannot scale horizontally but must be performant, such as game engines, OOP seems to make sense since it makes mutation easier to handle by hiding it with encapsulation.
I have a hypothesis that multiple core processors and cloud computing led to the current surge in popularity of FP.
0. https://en.wikipedia.org/wiki/Functional_programming#Efficie...
For example, I always found it ironic that the JavaScript library Immutable.js uses OOP heavily, but I'm guessing this was for performance reasons.
Do you mean it allows computation to be parallelized more easily? Compared to what?
How does "referential transparency" affect the situation one way or the other?
2. Real OOP and FP do not operate on the same level. Functional, logic and imperative programming are about language design. OOP is about system design. C++ and Java poisoned the concept. Listen to Alan Kay. Note that he openly admits that "objects" as they were implemented in Smalltalk are too small. Good objects are probably somewhere in between Ruby/JavaScript object and ad a microservice in size/complexity.
3. You can implement an OOP system in an FP language. Elixir/Erlang are a good example.
---
Side note: most of what Kay said about system design was validated many times over. If you think you know better than him simply because you use command line and functional programming, while he talks about graphical interfaces and OOP, you are missing the point, big time.
http://www.norvig.com/design-patterns/
What I would recommend is learning about the Smalltalk approach (not necessarily the language, but how Smalltalk environments are designed and what you can do in them). Another thing I recommend looking into is Agent-Oriented programming.
Agents seem to be much closer to Kay's original vision than anything we call "object" today.
Here is a really cool read:
https://alumni.media.mit.edu/~mt/diss/prog-w-agents.pdf
It might sounds academic, but there are tons of great design ideas there. I used some of them in HR applications, which is as "boring" and practical as programming gets.
How is a Ruby/JavaScript object different from a Java one?
This describes most programming languages. Actually, on average it's usually a bit worse. It's a set of nifty-on-the-surface ideas which are also subtly bad, which put together result in emergent really-bad.
Programming languages are like bands or sports teams. The constituent parts might not be the "best" field leading examples, but put together the whole gestalt can still be awesome. It can help if there's one part which is attention getting, but the whole thing has to "jell."
You can implement an OOP system in an FP language. Elixir/Erlang are a good example.
Lisp as well.
The biggest FP/OOP idea I've seen in my career has to do with awareness around dataflow. If your app is actually about dataflow, then dataflow and explicitly representing the relationships is key. I've seen lots of bad OOP thrown at dataflow, where only few of the relationships are explicit, and you "just have to know" how information gets from one part to another.
I agree and its why I don't like OOP. Either you are thinking in terms of putting things together or tearing them apart. Complex means to intertwine or put together. OOP thus increases complexity and a well defined OOP system makes increasing complexity easier.
I, personally, prefer simple. Simple systems takes more work up front, but are easier to read, reason about, extend, and maintain.
People tend to think of OO as a way of encapsulating data and then providing access to it. I rather think of it as a way of encoding program state. Often you have multiple operations that you want to perform on the same program state. It is nice to group that code together. Indeed, we have a computer sciency word for that: cohesion.
In FP, too, we do exactly the same thing. Take a look at any modern FP program and you will see that it is usually broken down into modules that represent program state. The modules contain code that operates on that state. We do it for the same reason we do it in OO: it creates good cohesion.
Getting back to my original question: why not structs with multiple dispatch? Actually, I wouldn't mind that at all (what's not to like about multiple dispatch?). However, the problem, I actually have is the struct. It's the same problem I have with the way many people approach OO: they are thinking about the code from the perspective of how to encapsulate data, not about what functionality they want. They are thinking about raw data instead of program state. A struct is a fine place to store data, but it's not the first place you want to go when you are doing design.
Instead you want to build your functionality and identify where you have commonalities in state. Then you should start thinking, "Hey these are using similar state. Are they related? Would it be more cohesive if I grouped them together?" Of course, it would be kind of strange to do that all the time. Most experienced programmers have a pretty good idea what program state they need to work on before they start. They can design data structures that are likely to be needed ahead of time. However, the priority should be to adjust the data structures to match the needs of the program state and to create more cohesive programs -- not to provide rigid boundaries separating your code.
Of course, nothing I've said is much different than programming in FP, and I wish that were not a surprise. OO and FP are not really so different. It's pretty easy to make the exact same mistakes in FP as you can make in OO. I sometimes feel like FP is just not popular enough to have widespread bad practices (like writing entire applications using only free monads).
That's very consistent with the original definition of OO, which hardly anyone uses much anymore. And erlang and elixir (and Ruby if you follow Gary Bernhardt) both fps do that quite well, via the actor model. Where OO starts to fail is when you gave to make a decision between two objects, e.g. "knight attacks mage" is the "attack" function a member of knight or a member of mage?
Grouping functions by what state they touch is basically just namespacing, common between OO and FP.
In class- or prototype-based OOP, we have to choose between `knight.attack(mage)`, with the functionality living inside `knight`; or `mage.defend(knight)` with the functionality living inside `mage`.
> The action (verb), attack, is a function which makes decisions and has portability.
OOP discourages standalone functions, so this function would either have to be in the knight, in the mage or (I would argue even worse) some sort of `GameState` object.
> OOP discourages standalone functions
Oh how I detest OOP. Portability of atomic components is amazing. So is scope nesting. Its like driving to work whenever you want in any of your cars (or your neighbors) instead of checking out a car from a car model inherited by a sedan class inherited from Toyota car class when such a thing becomes available.
> The same instruction set would apply if it were knight attacking mage or mage attacking knight.
Not necessarily! For example, a mage might cast long-range magic, whilst a knight would only have melee attacks. Where we make this distinction can affect our understanding, maintaining and extending of the code.
Standard OOP practice would use an inheritance hierarchy to handle this difference, e.g. a `Character` class could have `RangedCharacter` and `MeleeCharacter` subclasses whose `attack` method handles the details of those sorts of attack; then `Knight` and `Mage` subclasses those, to specialise the parameters. The major problem with this is that we can't model multiple distinctions in this way; for example knights and mages might both be "noble", whilst trolls and imps are "evil". If we want `Knight` to inherit from both `MeleeCharacter` and `NobleCharacter` we'd need multiple inheritance, which is where things get tricky.
More recent trends favour composition rather than inheritance, e.g. passing in an `Attack` argument during construction, with `RangedAttack` and `MeleeAttack` subclasses. Separate objects can be passed in for the `Noble`/`Evil` distinction. Of course, these are just standalone functions with more boilerplate ;)
Mixins are somewhere inbetween, where we avoid some of the boilerplate chaperones ( https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo... )
The CLOS approach, mentioned by others in these comments, lets us define verbs like `attack` outside of any particular class or mixin. We can then define it case-wise based on its arguments, like `attack(RangedCharacter, Character)` and `attack(MeleeCharacter, Character)`.
For me that is just a difference in data properties. The event is the actual attack. A ranged attack would be the distance between the two parties. A melee attack would have a distance of 1. A self-inflicted attack would have a distance of 0. If you attempt a melee attack from a different room the attack action still happens, but the other party receives no damage and probably no awareness of the attack. The handling of the action and consideration of the data is still the same regardless of who commits the action and who is the attacked party.
If you define a generic function ATTACK with arguments ATTACKER and ATTACKED, you are free to create a method with specialization ((attacker knight) (attacked mage)) and write code that will be executed only when a knight attacks a mage.
For me, it's a member of the Fighter protocol/interface to which they both conform, and either might override the default implementation or leave it be.
Exactly. And at the end of the day all you're really doing is defining a function. Why do you need a class at all? Is it just because the language you're using doesn't allow standalone functions? If you're defining a static function that has no state on a class, you don't need the class at all. It's just a function that you want.
battle(knight, mage)
Simply adding an `addDamage` method to the above system exemplifies where OOP shines. The above method works at lower, domain level, where the semantics of adding damage are specific to the object to which damage is being added.
The point here is that _both_ paradigms should be employed.
Unfortunately in most OOP langauges, this isn't an option. Free functions aren't a thing, for the most part.
An object is the smallest unit, so if you need a single function you end up creating an object to hold it. Which results in an explosion of FooHelper, FooUtilities etc
This objects as the smallest unit also can end up in an explosion of regular classes, all only slightly different.
It's madness.
Even extension methods need to go in a class (albeit a static class).
It would be nice if you could define functions that lived in a namespace.
C# now even has support for omitting the need to type the class name because you can import it as if it were a namespace (`using static`). Seems like a trivial complaint.
These considerations are actually becoming more relevant to FP rather than less, especially as we're slowly gaining a broader understanding of the potential of newly-developed features such as homotopy types, in enabling one to write programs that can work directly with such isomorphisms, ensure that desirable encapsulation-like properties hold, and even convert code to work "across" any arbitrary change in internal representation.
I wish I could make people at work realise this.
IT's all about do what is easiest now, rather than put the work in up front to make sure it's easy later.
All our codebases devolve into a tire fire as every change is made in the way that it is easiest at that moment.
I think they assume this is the cheapest way to run, by just kicking the maintainability ball down the road continually. BUt that ends up building up drag on all of our code that eventually makes making even small changes so complicated, slow and error prone.
To the opposite many organizations in my personal experience view it as a waste and a risk. This is often because they are only rewarding enhancement/defect user stories, testing the wrong things (if they are testing at all), and placing too much emphasis on weak diff tools as a substitute for an actual code review.
When you are only rewarding user story completion those who complete the most stories (not how) are the most rewarded. If your team contains insecure people it means achievement by code style, otherwise it means to do what is fastest. If you are locked into stories by sprint cycle it means lots of time to stare out the window.
I've been programming for over 20 years and I have yet to encounter such a thing.
They have always ended up devolving into a complete shit show.
For example, am I going to minify for beautify code and am I reading from a file, stdin, or a directory and am I going to output the actual processed code, a report, or something to stdout? If I were doing a comparison then I to make those same decisions on two separate sets of data and consider additional decisions.
In this horrid example its decision hell plus callback hell and the result is a challenge in flow control.
While Common Lisp's CLOS is often referred to as OOP, it's definitely less coupled than single dispatch.
What I find limiting is easy access to efficient, immutable data structures. Ocaml leaves you with the option to switch to mutable data structures at the cost of safety, and Haskell provides things like the ST monad for handling mutable updates safely at the cost of type-level complexity.
And solving the same kind of problems without polymorphism (be that multiple dispatch, type classes or otherwise), is a struggle.
Protocols or typeclasses are also ways to implement functionality over (usually) immutable data. I don't see the relation to a mutable object with a closed set of methods defined at compile time.
Go back to my original comment and you'll see that multiple dispatch is what got us here.
I never said anything about mutable/immutable data, I'm talking about polymorphism. But I did mention type classes as an alternative.
I frankly fail to see how this is leading anywhere.
Multiple dispatch means functions are generic, and all arguments are considered when deciding which implementation to call.
Snigl supports both multiple dispatch and pattern matching, as does Common Lisp.
The way it works is that, let's say you are modelling a pump. The pump is made up of a motor, a shell, the fluid with its own conservation equations etc. In such cases, inheritance and aggregation are godsends that allow us to quickly compose real-life objects as aggregates of easily modified and understood sub-components. Also, once you've taken these sub components to a point where you trust them, there is no further need to spend any time thinking about them. They are there, and you trust them, and you can reuse them and combine them with confidence.
More than anything, it gives us a great way to think about the modelling problem. The success of Modelica speaks to the superiority of this paradigm over things that came before it.
Just because you use FP or some other paradigm does not automatically mean you will get it right. I have seen poorly modelled solutions written in many different languages. I have also seen elegant solutions implemented in a variety of forms as well.
I think that having some depth of experience in a language, as well as a solid grasp of the problem, and perhaps some anal retentive tendencies (i.e. an innate desire to keep things very structured and consistent), can go a long way to solving problems elegantly :-)
My main point, though, was that these new languages are way better than Simulink etc. that went before them.
So much easier conceptually than trying to use the math to abstract it all down.
Multimethods allow dispatch on more than one argument (versus one ('this') for traditional OOP). Typeclasses decouple behavior from type hierarchies like Java interfaces do, except that they are extensible.
OOP is orthogonal to FP. That said, I'm increasingly of the opinion that it brings little to the table when a language already has typeclasses. I primarily use Scala, which supports typeclasses through traits and implicits. Just about every time I've modelled something in a traditional OOP way with abstract classes and such, it winds up becoming brittle and I refactor the behavior into typeclasses. I got burned by this enough times that I literally never use OOP hierarchies in my code, except to model algebraic data types.
Note that typeclasses and multimethods aren't fundamentally specific to FP, but they are mainly found in FP languages.
* For domain language (entities, services, value objects) use OOP as it is better at communicating intent
* For data processing and low-level algorithms (usually happens inside OOP methods) use FP style as it is less error-prone.
* Use FP when you need to build something purely computational.
* Use OOP when you want/need to build something generic for other people to use.
* But if you are application or game developer, ECS way of thinking brings a lot of benefit to the table.
I wouldn't go back to domain modelling in a oop language after that.
The insight that businesses decouple data from procedures is right. But SQL has poor support for functional hallmarks such as recursion, functions as first-class values, etc. SQL is not functional but declarative: it's less Haskell and more Prolog.
I think the author is using "functional" in a different sense from its usual use nowadays. Basically I think he just means that with SQL, you focus on the operations you're performing (i.e., functions), and the functions are only loosely coupled to the actual data, since you can write many different functions (SQL statements) on the same database, and the same SQL statement can be applied to many different databases with different underlying storage architectures. But of course, as you note, SQL being "functional" in this sense does not mean it supports more recent features associated with functional programming.
Edit: the argument seems especially galling given that logic languages seldom get credit as "industrial strength" and functional programming has a huge following of "language geeks" when a logic programming language is what works-with a broad swath of regular languages.
[1]Also, John Hughs' manifesto, Why Functional Programming Matters published in 1990. https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.p...
That's called "declarative" -- not "functional".
And that's exactly what SQL does.
You specify what the result should be like "I want the result to have those two sets common elements, but only items where this column is larger than 10, and I want it grouped by these 2 columns and ordered by this over column", and now how to get to it (e.g. open these files, read those indexes, load a hash table with matches, iterate over them to find those where column > 10 etc).
GROUP BY x and WHERE X > 10, etc. are not "the operations performed to obtain the result", are high level descriptions of the result we want.
In fact in CS courses, SQL was one of the canonical examples of declarative programming (despite some nits to the premise, e.g. the ability to specify use of indexes etc).
I find the FP vs OOP to be more of a discussion about "approach" more than anything else, where the language is more of an implementation detail. When understood like this, I don't think it's unreasonable to call SQL functional.
So an SQL UPDATE statement is purely declarative? "Update" sure looks like an operation to me. Similar remarks would apply to DELETE or INSERT.
For queries, I can see your point of view, yes; but even for queries, SELECT, GROUP BY, etc. can be viewed as operations just as well as descriptions of the result set. (WHERE, I agree, doesn't really look like an operation.)
> GROUP BY x and WHERE X > 10, etc. are not "the operations performed to obtain the result", are high level descriptions of the result we want.
Again, I think this is a matter of point of view. GROUP BY isn't a low-level operation, sure; but that doesn't mean it's not an operation, or can't be viewed as one. High-level operations are still operations.
> in CS courses, SQL was one of the canonical examples of declarative programming
I'm not trying to dispute this. I'm saying that given how the author of the article is using the term "functional", it seems to me that SQL could be considered "functional". But I've already noted that the way the author of the article is using the term "functional" is probably not the way that term is commonly used.
The Update statement tells "set this column to this value". It's totally a declaration of intent.
It doesn't specify anything about how the update is going to happen behind the scenes (open this file, use this tree structure, traverse, find the nth entry, write this value to the disk, and so on).
(Databases also support PL/SQL which is more imperative in nature, but here we're talking about SQL the query language).
>but that doesn't mean it's not an operation, or can't be viewed as one. High-level operations are still operations.
Operation in the context of declarative vs non declarative is not about high vs low, it is about whether the language has the user explicitly define control flow.
In the "group by" the user just tells to the DB that they want the results grouped, they don't specify any control flow, or tell how that grouping will happen.
"Set this column to this value" sure looks like an operation to me.
> It doesn't specify anything about how the update is going to happen behind the scenes
Neither does the expression y = f(x). But that expression is the canonical example of a function, not a declarative statement.
> Operation in the context of declarative vs non declarative
Is whatever you are defining it to be, I get that. But I don't think the author of the article is using words the same way you are.
- Let data be relatively static, immutable, objects (just as the post suggests)
- For functions which are merely read-only views of this data, implement them as methods on the class (An example would be "Volume()" on a data object representing a geometric object)
- For functions that changes the original data on data objects, implement them as separate functions (can be encapsulated in asynchronous black-box processes ala flow-based programming if you like), returning new, immutable instances of the objects they operate on.
This allows keeping data objects immutable, while still allowing you to gather certain frequently used calculations on the object itself, for code readability / maintainability, as long as they are read-only.
The reality is we should be much more flexible with what languages we choose to solve problems, if we want the flexibility to best match our problems to the right design paradigm.
Failing that, it's better to go with one "kind" of programming, the one that fits the language. At that point, we're hunting consistency, not the platonic ideal of the solution.
Unrelated, but it seems like people here were sold OO as magical or somehow "easy". It's damn hard to get an OO design right, and if it's not working for you, it's probably you and not OO. You probably missed something, which is not only reasonable, but expected. That's where the experience, skill, and expertise come into play.
Is this accurate? I would see it more as FP separates the two abstractions that OOP (for better or for worse) would have us keep together.
With immutable objects you can't easily create longer loops in data (self-reference is okay though).
Is my data a tree of some notion, I always go for FP.
However, when thick clients gave way to web apps (ignoring SPAs for the moment) the industry still tried to pound the square OO peg into the round backend hole. Most of that backend code is just transforming data between a database and either a browser in the case of a web app or another client in the case of web services, applying business rules along the way. I think this class of software is much better served by the FP model.
With Reagent, you have reactive atoms containing your data and components subscribe to them. Whenever the state of the atom changes, the UI is automatically repainted. You basically end up with a classic MVC pattern where UI components dispatch events that update the sate, and observe the changes via subscriptions.
The advantage over OO is that the state is easily decoupled from the UI, and you can observe the entire state of the UI via data. Since the logic lives outside the UI components, you can also do testing at event level which tends to be a lot more stable.
That said, I think FP still has one disadvantage which is performance. This is because the underlying computer is 100% imperative and there are still a lot of work to make FP patterns efficient.
FP works well for interactive UIs because the number of objects that change in GUI is very small, so performance is not an issue. But take a video game, for example, then you have thousands of moving objects that are updated at 60FPS. Once you get that you notice is really hard to write the logic as a set of immutable state changes, object allocation becomes a bottleneck and then you discover that OO is more suitable, at least for now. Maybe later there will be ways to efficiently translate the FP patterns into the imperative CPU models.
It's also worth noting that modern hardware hasn't been using imperative CPU models for a long time, and it provides an emulation layer that languages like C target https://queue.acm.org/detail.cfm?id=3212479
I think that's 90% of it, actually. In no case in my life have I seen a company starting a new project say "We're going to examine the available {programming languages, databases, operating systems, ...} and pick the one which is most appropriate to our problem". It was definitely never even considered after more than 3 lines of code have been written.
Even upgrading from one version of a language to a newer version of that same language is, usually, like pulling teeth.
Thinking back over the past several companies I've worked with, reasons for picking a programming language included:
- $(first_dev) wanted to learn a new language
- $(first_dev) didn't want to learn a new language
- Company policy says we must use $(lang) (even though no other software runs on the same platform, or has anything at all in common with this new project)
- $(founder) used to work for $(compiler_company)
- We googled for a library to do $(task) and first one we found was for $(lang)
I don't know what to call this, but it isn't engineering. My only problem with calling it fashion is that I'm afraid it would seem disrespectful to the fashion industry.
I believe more powerful formalism has to be used. For example pi calculus or petri nets. In both you can represent the 'global variable database' as a process sitting somewhere and you communicate with it using channels or as a message passing in general. Lambda calculus is too constraining, because everything is a deterministic function and it can't model exactly this scenario. Another use case hard to model in lambda calculus is time. Eg. run some function after N seconds. Or plain 'sleep' function.
When you now look at programming languages in terms of, let's say, Petri nets, you see clearly how OOP (Alan Kay's message passing) is the part where you send messages between processes and FP is contents of a particular process.
Other than that, there are two types of OOP I noticed. 1. Alan Kay message passing. Originated from people trying to implement simulations 2. OOP as modules (Java, C++), aka 'Dog implements Animal' OOP. In this paradigm people try to come up with ontologies, ie. hierarchical models of data dependencies. Instead of using actual modules, people use classes for some reason. Then you can see module-like features implemented on classes, eg. private and public modifiers of methods.
'Dog implements Animal' should be modeled as 'Module Dog depends on module Animal'
For example, as many have pointed out, methods like "collide" or "attack" don't really make sense to have on a single object. This is because this method works at the level of coordination, and therefore _should_ be functional. Alternatively a method like "addDamage" method works at the domain level were it enforces rules/state change.
We can easily apply the above observation to system architecture and conclude that the outer-most layers of an application (services) are best-implemented using a FP approach and the inner-most layers (domain) are best-implemented using OOP.
It does not surprise me that most applications adopt FP. Because most people think procedurally and start applications with a top-down mindset, the outer-most layers (use cases) tend to be modeled first where a FP approach makes sense. This then just carries through the rest of the application.
Did you mean "OOP" instead of "FP" here? First, I think more applications adopt OOP than adopt FP. Second, I find it hard to go from "most people think procedurally" to "therefore they wind up at FP".
I would suggest that most systems adopt a procedural style where objects are more or less bags of data being passed around to functions/methods. This generally fits the mental model of how people think. That is, in terms of input -> output. OOP blurs that simplicity because each unit of behavior (method) also has a surrounding context (properties) that may be affected as well.
At the end of the day the difference between Object.DoSomething( data ) and DoSomething( object, data ) is just semantics (look at how python implements classes for example).
"Those that are unlikely to change but are subject to being operated upon by a changing cast of entities should be written in a more functional style, while those things that change relatively often can be written in an OO style"
Where the conflicts arise is when we have stateless functions. It's a bad practice from OO perspective to have stateless (or static) functions. I am yet to have a good solution to this problem.
What I found to work best is to build an OO data model and a (not necessarily functional) business logic layer on top. In pure OO languages like Java and C# this layer takes on singleton characteristics, which is a clear sign that this code should be simply procedural. The individual functions are often not very interdependent and easy to work on and test.
In haskell this is called mtl-style classes and quite common. Here is a blog post that describes how to split applications into an imperative, oop and functional layer. The oop layer uses mtl-style classes: https://www.parsonsmatt.org/2018/03/22/three_layer_haskell_c...
"you can tell if someone started game development as a hobby because they ARE using an ECS."
Very interesting article here: https://www.gamedev.net/blogs/entry/2265481-oop-is-dead-long...
However, if you're designing an open-ended game engine for other people to develop in, then an ECS is a godsend, because you have no clue what they plan to make their game do.
I thought they actually just use structs and whatever data structure works.
I still struggle to find areas where full-blown classical, class-based OOP is a good fit for the problem. Game development isn't such area.
Some claim (Like Gosling) that inheritance was never a major point of OOP and that it has been overused.
What do you mean by "tag" here. Is a "tag" implemented as a class in C++? Do you know of an example in real code that is open source, the the curious can study?
Google for "ECS", or "Entity-Component-System". It's a sorta-pattern for what I described. I say sorta, because everyone has a slightly different idea of how it's supposed to be implemented, but typically, entities are just dumb containers for (instances of) components, the components store data & type info, and systems mutate those components. So e.g. an asteroid in a game of Asteroids would consist of e.g. "kinematics" component, and "sprite" component, and a "collider" component. If you wanted that asteroid to drop a powerup on death, you'd add "drops powerup" component to it, possibly at runtime.
Components may or may not be implemented as classes, and the aggregation may or may not be direct. In small games where I used this (in Common Lisp), components were classes, and each entity object had a list of instances of those components. This was a naïve approach, but got the job done. A more performant implementation might look like this: each component is a struct, and gets a dedicated array of all instances; each entity only has a) tags of the types of components it consists of, and b) indexes into the "components" array. This optimizes for data locality, especially for more isolated systems. For instance, the physics system can now just loop over the array of "kinematics" components, and mutate their state, without knowing anything about actual game objects - and all the data necessary for updating physics for all game objects are close together in memory, reducing cache misses and allowing for other optimizations.
Note how this is a complete inversion of OOP - components are now storing just state, all behaviours get segregated into systems that operate on those components in batch mode, and entities exist only to tell you which instances of components together form a game object.
I don't have any reference C++ implementation handy to show, but if you read up on ECS, you're bound to find something.
Personally, at this point the way I write software is so far removed from this discussion that I just do what feels right and what people can generally agree on within my teams. Solve the problem, don't discuss code, discuss solving the actual problem.
Is a good post about how that can fail from the creators of starbound.
Relevant quote:
A new requirement comes in: I have an idea for a special kind of item that when the player holds it and its near a specific kind of enemy, it will glow. The enemies that trigger this should be scared of the item and back away, but have a special animation where they’re mesmerized by the glowing item. This should only work for players that have achieved some specific quest goal.
You throw up your hands in frustration [...]
FP: Server Side
OOP: Client Side
For example, CoreData in iOS has to be my least favorite approach to a data model yet. I spend a lot of time thinking "I wish this was Elm or React, it would definitely clean this up" when building iOS apps in general. Definitely feels like the state of the art has changed.