The Power of Interoperability: Why Objects Are Inevitable [pdf]
cs.cmu.edu
cs.cmu.edu
While there has unquestionably been some hype about objects over the years, I have too much respect for the many brilliant developers I have met in industry to believe they have been hoodwinked, for decades now, by a fad. The question therefore arises: might there be genuine advantages of object-oriented programming that could explain its success?
A big factor is that programmers tend to be idealists. They want code that's the absolute best. {code: goodness(code)=max}. OO is a way to take pretty terrible code and go from goodness(code)=X to X+1. But it doesn't guarantee getting further. It might even make harder to go to the next iteration. That's a terrible thing for any idealist but absolutely crucial in practice. Most code is "terrible" and most code embeds a lot of institutional knowledge that prevents it from being tossed away (see the tendency for disaster from "the big rewrite").
I would contrast this with the paradigm that's been "up and coming" for decades now - functional programming. FP aims to "start at the best and stay there". The problem is that even if we assume FP can do that, it's not aiming to solve the "increment the goodness" problem, which is what a lot of ordinary programming at ordinary companies has to involve.
I'd say there's a difference between purely functional languages and functional concepts in general. Purely functional languages have indeed remained relatively unpopular (I'm not really sure why, but I'd guess it's a convex combination of "it's not how the machine works", "it's relatively esoteric unless you have studied math or cs" and "not very much backing/support"), but we've also seen functional concepts "contaminate" traditional OO languages.
Yes, I know, it's not really "functional" and diehard FP programmers will hate me, but I'd still say it's evidence that the functional paradigm can be (and in fact has been) used to "increment the goodness".
This means that "functional effects" do sometimes do best as something like subsystems called by the main system.
My intuition on this:
Procedural/imperative thinking is more "natural" for most people. Asking someone to explain a process without any formal training in any formal language/modeling process on this (ie, a non-programmer) they'll give you a procedural/imperative description of what to do.
OOP languages are, mostly, procedural/imperative languages with extra constructs. To varying degrees these constructs are organizational as much as actually substantive additions to the execution model. For instance, classes in Java are modules for collecting related behavior and controlling access to (usually) private data fields. They can be seen as "rich" structs: data objects + procedures for altering the data.
Now, polymorphism actually adds a lot more and can be used to subsume other control flow mechanisms, I'm not saying that OOP is just procedural + organizational elements. But as a first pass, people can step into OOP by treating it as such and learn the rest as they continue.
FP languages, by contrast, do not start off as procedural + first class functions or expressive type systems. In particular, the pure FP languages drop the imperative notions (or obscure them). This creates a major hurdle for many would-be learners. Their intuition doesn't apply to these languages.
See also Prolog and relational programming for another area where people's natural tendency towards imperative thinking creates a major block on learning (or on using it to its potential).
Really, most people would probably be more at home coding in something like a safer version of C.
- mutability is always there, you're never sure something in the object graph will bite you back
- no clear initialization, object can be "constructed" but you never know what state you get in the end, you need to read every class implicit protocol to be sure you called the right methods after `new` (FP asks for near in-order static trees.. less cute but quite obvious about dependencies). that is unless people played fully with generics and classes (phantom types to express what step you're in the object state graph)
- too much bike shedding about private / public fields
- single dispatch forcing someone to own some logic when there's zero logical reason to do so
I feel like I got more done writing assembly when I was 13 than I do today.
So when you say that B is better than A, get specific. "If I choose B over A, I'm saying that testability and straightforward implementation are more important in the context of this project than concision. Is that true?" If it's not, keep A. If it is, remember your choice! Then when C tempts you, you can ask yourself- is switching to C consistent with the weighting of criteria that caused you to switch from A to B?
Perfect software doesn't exist. But you can try to best match your implementation to the particular concerns of the context it's written in.
Single dispatch is great/sufficient for modeling process encapsulated by a “single agent” but multiple dispatch feels far more elegant to encode interactions between multiple agents.
Eg: Neither a.sum(b) nor b.sum(a) is as cleanly extensible as sum(a,b)
https://www.amazon.com/Practical-Common-Lisp-Peter-Seibel/dp...
If regular old OOP beats everything, then True Scotsman OOP is just a footnote; oh, by the way, not only has single dispatch OOP eaten the world, but there are even better forms of OOP, which will lick the plate clean and ask for seconds. (But regular old OOP has been so successful that we have not had to massively adopt these.)
Now let's look at your sum(a, b). The paper talks specifically about service abstractions, and how OOP has specifically helped us manage them.
sum(a, b) is not a service abstraction. It's a functional abstraction! We add a and b, which could be many combinations of types (and so we need dispatch). But at the end, we will be returning a new object c, leaving a and b alone.
While multiple dispatch is all well and good, the paper's argument is, via anecdotes and hand-waving, that regular old OOP that your pointy-haired-boss is familiar with has been successful because it represents service abstractions well, in which services can be extended while remaining compatible.
Services is a code word for objects that change their state in response to requests. A.k.a. receive messages, change state and send replies.
Speaking of actual services, multiple dispatch has a poor story in distributed systems. It's difficult to send a message to three different objects in different geographical locations and get that to look like a single method.
When you have components from different vendors, multiple dispatch has gone out the window. Multiple dispatch requires classes to be completely open so that methods can peek into the innards of several classes. How will that work with server RPC API's from different vendors, COM objects from different vendors, you name it.
Multiple dispatch works well in large part because, at the bottom, a concrete piece of code gets dispatched, which has access to all the arguments and can do the work in one place, all within the same address space of one dynamic language image.
Single dispatch scales beyond process and machine boundaries. If you stick to a single dispatch design in the straw man prototype of a system, you will not have to redesign that basic aspect of the system to go to the wooden man that works across a network. Like oh shit, objects A and B of this double dispatch method are actually RPC proxies that live on different servers; now what?
And, by the way, that specific example is doable (somewhat) in languages with static dispatch on multiple arguments, like C++'s function overloading.
We're just stuck with a statically determined result type; C++ won't statically model something like that an division of two Integer classes yields an Integer, otherwise a Ratio. If we don't need that, we are good to go:
// no OOP dispatch to see here
quaternion mul(const quaternion &left, double right);
quaternion mul(double left, const quaternion &right);
Static polymorphism may be good enough for a good many use cases, and runs fast too due to all dispatch being resolved at compile time.Multiple dispatch will not save you from the fact that if you have N different kinds of numbers, you may have to code all N*N combinations of some operations that don't commute, or N(N+1)/2 for those that do. It will give you a minimal-boilerplate way to write that though, with accurate modeling of situations where the result type depends on properties of run-time value.
I say mostly, because something like the Observer pattern is still useful even outside OO. It's also a way to communicate an intent or a role a certain class has.
I think all the SOLID/DRY/Clean code concepts were more harmful to development in general than design patterns, because these ideals became an obsession.
C++ supports passing functions as arguments and closures yet factories are often necessary, how else are you creating new objects from dynamically loaded plug-ins ?
In an way, obkects are just degenrated swarms...
The whole idea is quite simple but maybe a bit too abstract... We proposed even an simple primitive called "swarm" that can be used to program. I had a old project called swarmesb (not active anymore) but this perspective got me to a lot more insights in another open source research project called privatesky... https://privatesky.xyz/overview/swarms-explained is an attempt i made to explain this way of seeing swarms.
However there’s a problem. It’s easy enough to send a swarm a message. Not so easy to understand back the cacophony it produces.
Many actions in the world are specific instances in terms of facts, despite being part of a larger category.
How would a swarm PayPal work like? John sends 10 to Jane. That’s one to one, not swarm to swarm.
If you look how things are organized in nature, there’s always a swarm synchronizer, bringing the many to one.
It can be avoided in simple probabilistic systems, but it emerges back with complexity. I don’t talk to a swarm of cells for ex. I address the single entity defined by your central nervous system.