The Repeated Deaths of OOP (2015)
loup-vaillant.fr
loup-vaillant.fr
Function calls and messages are, fundamentally, isomorphic. Messages were never definitional.
The notion of "purity", everything is an object, was never definitional. That was a Smalltalk conceit. Alan Kay named the idea, but stole it. So he doesn't get to impose his religion.
What is definitional? Inheritance, encapsulation, methods. But when you abandon specious purity, OO becomes one organizational style among many. Where it is the right thing, it is. Where it isn't, something else is.
Sometimes mechanisms invented for OO are useful for other organizational purposes, without bringing along OO—which is fine. Implementation inheritance? Sure, why not? That's "bad OO", but OO isn't sacred, so not blasphemy. It has consequences, but they are sometimes—often, even—OK.
"Data-oriented"? "Entity-relationship"? "Functional"? "Pipelined"? Other organizational styles, also useful.
In places.
OO will not die. The others will not die. All organizational methods have their place. Games can have pockets of OO. Simulations can have pockets of OO. Nobody is keeping score.
Purity is a farce.
I vote for just not using this term anymore and talk about class based langusges or prototype languages or whatever.
PL researches seem to have no trouble finding equivalences between ML modules and OO :
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.53...
My personal opinion which tries to be as general as possible to encompass most definitions is that OO just means that semantic entities can bind a set of named values with a set of named operations / functions / methods / whatever you call them.
struct foo {
int bar;
void (*compute)(int x, int y);
};
is also OO.This is all well and good as long as people are clear about what their definition is (although it is weird how many proponents have wildly different definitions and yet still seem to feel some kind of kinship). The maddening folks are those who deflect all criticism of OOP by saying “that’s not true OOP without ever asserting any particular definitional qualities. These people seem to have an undying love for the sequence of letters O-O-P. So we have these weird conversations where I say, “fine, I contend that anything that might make OOP unique from other paradigms (e.g., inheritance) is a misfeature”.
Publications by Hewitt or Sussman in the seventies even suggest that the concept traces back to lambda calculus.
> so Smalltalk-style messages are still messages
Only Smalltalk-72 had true "message passing" (i.e. arbitrary tokens sent to an object for interpretation); Smalltalk-76 onwards uses a virtual methods implementation similar to the one of Simula 67 or C++ instead.
Since Actors are universal primitives of digital computation,
it is possible to reframe all previous work as using
messages, including Turing Machines and the lambda calculus.
Lambda calculus was originally defined using string substitution.
Both lambda calculus and Turing machines are computationally less powerful than Actors.
Do you have a reference for that?
It doesn't need much discussion.
If your language forces you to use mechanisms and organization inappropriate to the problem, the solution will be ugly and awkward. In that case a better language, or more usually a better job, is in order. The requirements might dictate the language, but they don't typically dictate you being the one to do it.
What is inheritance but a way to encapsulate data and methods in a way where you only need to override a subset of very specific methods and/or data to specialize an implementation?
Your definition is closer to what the Template Method pattern [1] is, rather than inheritance in general.
Keep in mind also that neither inheritance itself nor the Template Method pattern guarantee encapsulation.
EDIT: changed ancestor to descendant, thanks gnufx!
Not Erlang (maybe depending on whether you consider something like Armstrong's universal server extreme late binding)?
https://www.quora.com/What-does-Alan-Kay-think-about-Joe-Arm...
Bit rich to say he "stole it" when he himself said it's just the foundational principle around which systems form (living or not). Which already implies he doesn't claim to have invented it.
It is common, when you steal an invention, to insist it is wholly natural, so not really any sort of invention at all. Saying it does not make it true. There was a time before; Kay came after.
The referenced article - like many others - moreover quotes Kay incompletely; Kay in the sentence next to the one quoted in the article said "the important thing here is that I have many of the same feelings about Smalltalk"; see https://www.youtube.com/watch?v=oKg1hTOQXoY&t=633s. Smalltalk-76 and 80 (in contrast to Smalltalk-72) implement inheritance and virtual methods as it was done in Simula 67, just like C++ and Java do.
There is also a well known clear answer of his to the question of what is OOP to him. We ended up with something different in real life, but it's pretty silly to accuse somebody of stealing an idea and giving it a name, when they wanted to do something else. If anything, the name OO was "stolen" to refer to something not originally intended. But even in this case the word steal is too harsh, and your general style leaves a really bad impression.
Practical reasons for that success—independent programs that run at native-machine speed, using native-machine data representations, and intimately compatible with an existing, successful industrial language—were chosen with careful deliberation, leaving certain Lisp strengths—particularly, function literals—aside, for later. It seemed like the right approach, at the time, and turned out to be.
Java could have been the practical Smalltalk, but its designers elected instead to ape superficial details of C++, for no defensible reasons, to Java's detriment.
The good ideas in Smalltalk have not had much effect on the world. All of Smalltalk's weaknesses were inherited directly from Lisp, leaving it with no better prospects than Lisp has had, or has, for identically the same reasons. But it turns out the "object orientation", like any other exclusive organizational principle, is an inadequate basis for a language meant for broad use. So, C++ supports OO and other styles, and even Java has begun to branch out.
Subsequent development of C++ has drawn mainly upon ML, fleshing out support for other organizational styles that Smalltalk and Lisp never attempted. And, it got lambdas, after all.
In actuality, it went probably closer to: "I'm gonna make a textual preprocessor for C that gives it Simula-like classes, for shits and giggles".
It was, instead, the first of what became many, many compilers that generated C from its AST as the target assembly language, relying then on the C compiler's optimizer and machine-code generator.
Nowadays, LLVM IR fills exactly the role C once did, for new languages like Rust and Zig. Zig is just now in the middle of transitioning to its own optimizer and machine-code generator.
Preprocessors were also common at the time, e.g. m4, Ratfor, and Cpp ("C preprocessor", which initially was a separate program), so it is not surprising that Cfront was mistaken for one, back then.
That is not a reason to repeat the falsehood.
You don't get a runaway success for four decades by accident.
That qualifies as a preprocessor to me.
You can get the sources to it, as late as 3.0.3 from 1994 here.
http://www.softwarepreservation.org/projects/c_plus_plus/ind...
Otherwise every compiler is a preprocessor, and the word is meaningless. Making words meaningless could be a way to make yourself feel important, but it does not advance understanding.
Since Cfront, up until LLVM, almost all new-language compilers emitted C code. They were not preprocessors. Compilers that emit LLVM IR are also not preprocessors.
My root comment is strictly about the initial impetus and proof-of-concept implementation, not ISO C++20.
Oh look, here is a quote from Stroustrup's HOPL paper about the history of C++:
In October of 1979 I had a pre−processor, called Cpre, that added Simula−like classes to C running and in March of 1980 this pre−processor had been refined to the point where it supported one real project and several experiments.
I think, if it's okay for Stroustrup to say "preprocessor" (and even put "pre" in the thing's name), it's okay for me.
If we put classes and prototypes in stark opposition, we can say yes, classes are not foundational. But what's the main distinction between classes and prototypes? One is defined statically and other dynamically. Static/dynamic distinction is a matter of optimization, it's not fundamental to the concept.
So I think when someone says "classes" when discussing OOP, they also imply prototypes, templates and all other forms of "molds" for objects that provides for their initial definition when created. In living systems we'd call this DNA. In engineering it's the combination of technical specifications and the equipment we use to materialize them in a factory.
The reason OO is a powerful organizational too is because it mirrors how many people naturally think about the real world. But inheritance as adopted by most simula-derived languages does not exist in the real world. What does exist in the real world is prototypal inheritance (specifically through cloning, not through sharing a reference to the prototype object as we see in many Self-derived languages). We see it all over biology, manufacturing, even cooking. Is a language in which cloning a structure is the primary way of achieving inheritance still OO?
> Programming using [abstract data types] is also known as object-based programming. Even though ADTs do not directly support inheritance and polymorphism, these concepts can be layered on top of a ADT implementation with some minor effort. In other words all important OOP concepts can be implemented in OBP.
However, if I believe that your assumptions about the event won't reify the event in a way that mirrors my own then I will want to override your assumption by communicating extra properties about the event. You can dive a bit into this by way of semantics.
For example, the semantic frame 'cooking'[1] lists 'fry' as a member. What are the salient differences between the parent frame 'cooking' and its member 'fry'? A quick way to explore this is by examining the propositional differences between the frame and member roles. You can imagine the differences between the Instrument and Result roles and how their assumptions differ.
How does this relate back to programming? One way to examine programming languages is through their ontological semantics. As much as we write programs for computers, we also write them for other people. Prototypical inheritance mirrors the structure of languages we already find in nature.
[1] https://verbs.colorado.edu/verb-index/vn3.3/vn/cooking-45.3....
You even get an inheritance hierarchy when pluripotent cells divide and become differentiated; the new cell gains functionality missing from the original as genes are turned on.
And yes, there's lots of composition too -- a cell is composed of cytoplasm, organelles, and a membrane (which serves as an interface to the message bus, your blood). The mechanism by which the cells are copied does involve DNA, but it and the proteins involved are an implementation detail.
Well, yes, anything inside the cell is, when modeled as an object, an implementation detail. External behavior is all that matters.
What always strikes me funny about prototypal inheritance is that it seems like a special case of composition, one that just happens to do all the necessary delegation for you.
In the case of the cell, I can model mitosis by having the two cells name their parent as the prototype, or I can define in the code that they're composed of the same stuff and delegate method calls the same way. The end result is the same, but composition is more flexible.
In JavaScript, prototypes have less boilerplate, but it's possible to have a language that eliminates most of the boilerplate for delegation as well (see Kotlin's delegation feature [0])
What do I gain by using a prototype instead of composition with delegation?
I’ve seen this said, but never justified. Is there anything that supports it?
It's a good fit for a microwave but not for a door.
It's a good fit for a person or anything with intelligence but arguably OO is _not_ a good fit for an object.
Do you describe the world around you using nouns? Anything you can name is an object. When you hear me use the word "pencil" you know I am describing something that's used for writing on paper, containing a substance like graphite that is probably erasable, and maybe even has its own little eraser on the end. Naming the object allows us to communicate and reason about it together; the ability to name and classify objects is the most essential element of OO (imagine a language where you can create classes but not name them; would it still be OO? I would argue it is not).
Of course this is not the only way to model the world. Maybe to you the act of writing or drawing is more important; the pencil or pen or marker is secondary. Or maybe the connections between objects is important to you. Or perhaps it is not the objects that are important (i.e. those things which are acted upon) but the subjects (those things which act). Whatever model feels most natural to you is probably valid. As the saying goes, "all models are wrong but some are useful"; this applies to the OO model as much as any other.
I've never found myself thinking that my pan fries an egg. I fry an egg and use the pan and the stove to do it. I've also not found myself thinking that my pan hits an intruder and dents itself. I hit the intruder (well, if I had the courage) and the resulting 'data/object' now has 'dent' as a property.
The idea that my coffee machine is making coffee, or that my combo-oven is making my food is perhaps a bit more palatable, but even then it's now how I think about things. I am the one making coffee, or heating up food, and the actions needed are not inherently part of the object.
IMO, the problem is that people do NOT think about the world in terms of objects, at least not in a way that conforms to what is generally considered OOP. Rather, when we think of 'objects' we generally think of data structures that we then operate on. The idea that there are objects that contain both properties/state and behavior is pretty weird irl.
You just call coffeemaker.brew(), oven.defrost(“meat”, “500g”), or car.accelerate(). From that perspective the OOP model works pretty well.
From the perspective of Procedural Programming this also works well:
brew(coffeemaker), defrost(oven, "meat", "500g"), accelerate(car).
Same for FP:
brewed(grains), defrosted(meat, time), withAcceleration(car).
And frankly, I don’t see the divide between “data structures and how we operate on them” vs OOP. They are not mutually exclusive. Some problems are data-first, others are behavior-first — so just be familiar with both and use the correct tool for the problem.
Still, the fact that you refer to the coffee machine as a noun and not a process tells me that you do think of that device as an object. And yes you do put the water into the machine and turn it on, then it produces coffee as an output. Some people might rightly say that makes it a function; others might rightly say that makes it an opaque object. That both views are common tells me that there is something natural about both ways of thinking.
I think Billy Idol sings about that in 1983 or so in White Wedding: "there's nothing pure in this world; there's nothing sure in this world..."
I am probably missing something, but how is inheritance essential for OOP?
In my understanding, inheritance is not necessary. The point of OOP was probably encapsulation. At some time inheritance became popular for "code reuse", until a preference for composition over inheritance developed.
Emphasis on inheritance is what infuriates me about many traditional sources about Java programming etc.. My java textbook says "object model" is major detail and "typing" is a minor detail. I am like, WTF, you cannot understand java object model until you understand type system.
We've learned a lot and incorporated a lot of ideas, machines, and concepts into the mainstream over the past few decades. But one thing that hasn't stayed the same is what set of things fits into what grab bag encompassed by a word someone came up with decades ago to describe the state of things at the time.
Once you start arguing "OOP" vs xyz or "FP" vs xyz, you've missed the point.
I think if we were intellectually honest in our arguments this would be fine—we could manage that OOP is poorly defined. Instead almost every debate about OOP takes this form: someone criticizes things which are exclusive to OOP (e.g., inheritance) or very common in OOP (kingdom of nouns, everything having references to everything else i.e., bananna-gorilla-jungle design or etc) and the OOP proponent argues that these things aren’t true OOP or else that “this is just bad code; anyone can write bad code in any paradigm”. In other words, OOP proponents reliably turn the debate toward semantics rather than engaging with the substantial criticism (if it is “just bad code” why does it seem to be so much more prevalent in OOP than FP?).
We could have a more interesting, robust debate, but poorly defined terms + bad faith means we get the same no true Scotsman rhetoric over and over, and it’s hopelessly dull.
To your point, critics of OOP could do more to anticipate these semantic arguments. Rather than using the term “OOP” they could just criticize features directly or they could give their definition of OO up front.
This is exactly what they must NOT do. If one is going to criticize, criticize the actual bad concepts, not the (poorly defined) paradigm. OOP and FP are effectively undefinable, and rightly so since they have outlived their usefulness as vehicles for introducing their respective "bunch of concepts".
Nobody's going to agree in the modern era what OOP or FP encompass, and anyone who attempts to define them will rightly be lambasted for it.
But we CAN agree on the definition and argue over the merits of concepts such as inheritance, immutability, lazy evaluations, closures, encapsulation, monads, etc. And the fact that languages are taking concepts from both "camps" shows how they were never actually opposed; merely components of different schools of thought that happened to contain some incompatible or opposing concepts.
It’s perfectly fine to define a shorthand for a larger concept, (even if that word has meanings in other contexts) so long as it’s clear what we mean by the term. We do this all the time in our writing and oratory (e.g., the biology sense of “immortality”).
The problem with OOP (and to a lesser extent, FP) is that we don’t make it clear what we mean by those terms and that makes for more misunderstandings between good faith actors and it gives bad faith actors more opportunity to drive the debate toward semantics. Being explicit up front solves that.
> Nobody's going to agree in the modern era what OOP or FP encompass, and anyone who attempts to define them will rightly be lambasted for it.
We don’t need to agree. We are free to define our terms. If I define OOP as X and criticize it accordingly, and you say “but my definition of OOP is Y so your argument is invalid!” then you’re very poor at reasoning and for all intents and purposes you’ve lost the debate.
If you want a healthy and robust debate, dismissing defenders of the most popular paradigm as always acting in bad faith is not how you get there.
This would also be a good way for you to learn more about the field. For example, is “just bad code” actually more prevalent due to OOP or because it’s the default paradigm for the most popular languages (Java, C#, Python, even JavaScript although that’s messy) and especially those used by the legions of lower-skilled developers cranking out business logic? Does FP reliably produce better results or is that more a function of the areas where the style is the most popular and, especially, a selection bias for developers with training & motivation to work on more than basic Jira-mill projects? (Lisp had a similar challenge for comparison: a large chunk of the developers you could hire were graduates of elite CS programs who would have performed above average with anything and it’s really hard to run a control study where, say, half of them spent a year building the same system in VB)
Note that I don’t think there’s a simple Boolean answer to any of those questions. I think FP is a useful technique but, like any tool, it is not perfect and a good developer should have enough experience to be flexible based on the problems and resources.
(1) I'm not dismissing, I'm criticizing. (2) I'm very clearly not arguing that all OOP proponents are always acting in bad faith.
> This would also be a good way for you to learn more about the field. For example, is “just bad code” actually more prevalent due to OOP or because it’s the default paradigm for the most popular languages (Java, C#, Python, even JavaScript although that’s messy) and especially those used by the legions of lower-skilled developers cranking out business logic?
This is begging the question. The whole point is there's a semantic argument: is OOP defined by the features and design patterns (including the "bad code" design patterns) that are unique to OOP, or is OOP defined differently and this is the same kind of "bad code" that exists in other ecosystems?
But in whatever case, the debate rarely advances this far because we get mired in semantics, which is my broader point.
> I think FP is a useful technique but, like any tool, it is not perfect and a good developer should have enough experience to be flexible based on the problems and resources.
To be clear, I'm not an FP purist. I actually don't like very many FP languages, and indeed there are some values of "OOP" for which I could be called an OOP enthusiast (to the extent that one would be willing to call idiomatic Go or Rust, "OOP").
You made sweeping unqualified assertions which are hard to reconcile with that claim:
“OOP proponents reliably turn the debate toward semantics rather than engaging with the substantial criticism”
“We could have a more interesting, robust debate, but poorly defined terms + bad faith means we get the same no true Scotsman rhetoric over and over, and it’s hopelessly dull.”
If that’s what you believe going in, it’s unsurprising you’re not finding better quality outcomes.
It’s not what I believed going in, it’s a summary of my cumulative experiences.
It goes without saying that there are no universal standard solutions. As an engineer, you learn how to proceed in a structured and systematic way and (hopefully) apply appropriate methods and technologies that fit the problem at hand. ECS is a good approach and there are many problems where this is a good fit; however, there are also problems where it is a less good fit. The same applies to OOP and all other methods of software engineering.
A "Component" is normally implemented as a struct, and a "System" as a standalone function. No encapsulation here.
Of course you would have to implement all of the above using classes and objects if your language doesn't have support for structs or standalone functions, but that's just an implementation detail. The concept itself is not a subset of OOP, and breaks many "rules" of OOP.
… it does seem to me that … OOP represents the discovery by the mainstream community that it is a good idea to associate code with data, but, since they still don't know how to do closures, they have bodged it. — Robin Popplestone, <2h6lcoINNkf7@ymir.cs.umass.edu>, comp.lang.functional
Also Java has them.
If you take the remark seriously, it's probably not just a question of having closures but what you (can reasonably) do with them. A Schemer might experiment with implementations of different object systems, Smalltalk-like, prototype-instance (about a page of code), and CLOS-like. Do people do so in Java (or even Smalltalk) and what does it look like if so?
Some of this has to do with the fact that the virtual machine for many Smalltalk implementations is implemented in Smalltalk itself. So you can start with Smalltalk and end up with something modified enough to be considered different.
You have objects that you iterate over and apply changes to the struct or object's fields like x and y coordinates in a game tick for the "next state"
Essentially you end up with lots of global functions and very large API space that has no ordering. There's inherent ordering to global function application to produce the correct result. So it produces complex APIs.
If you move the global functions to the entity objects themselves, you end up with OOP. So I'm not really sure what the author is arguing for.
ECS seems to me more like a pattern than a paradigm.
Edit: to clarify, a system can just be a function, an entity just an ID and a component should be plain old data anyway, so a struct is perfect.
Correct. This is exactly how most ECS frameworks are implemented today (except when it's implemented in languages that doesn't have those features, of course).
Any language with mixins and reflection can do ECS. e.g. Use mixins (components) and reflection to check which mixins an object (entity) has, then executing a code path (system) based upon that answer. Many dynamic languages, like Ruby, support this combination of features. I think the issue is many game developers work in C++ which lacks them.
There is a school of thought that design patterns are actually missing language features [1]. ECS seems like a text book case.
[1] http://wiki.c2.com/?AreDesignPatternsMissingLanguageFeatures
The fact that ECS can be implemented in OOP languages doesn't make it an OOP pattern, only that OOP languages have Turing-completeness. You could also write Pure-FP code or implement Prolog in an OOP language, or vice-versa.
> If you move the global functions to the entity objects themselves, you end up with OOP.
You're probably talking about moving the global functions (also called Systems) into the the Component objects. Entities in ECS are normally just integers to which components are attached to, the global functions you're talking to correspond to
This is possible for the simplest usages of ECS, however this is very limiting. One of the most important aspects of ECS, especially in modern frameworks, is the possibility of Systems (the "global functions") to operate in more than one kind of Component at a time.
For example: a collision detection System might want to operate on both "Position" and "Visibility" Components to avoid performing collision detection on invisible components.
This ability is crucial to ECS for two reasons: to have good separation of concerns, but also for performance.
With your suggestion in OOP, you would have to do that with multiple inheritance, or maybe by picking one Component to host the System and break encapsulation.
Its really not a stretch for OOP. Its just a design pattern.
Like I said, it's possible to implement anything that is computable in any Turing-complete language. The fact that there are some ECS implementations in OOP languages speaks more to the popularity of those languages, rather than their capabilities or suitability to having ECS implemented in it. Of course ECS is going to need classes in languages that don't allow structs. Of course ECS is going to need classes in languages that don't allow freestanding functions. But that's doesn't mean that everything that is implemented in an OOP language can be considered just another OOP pattern.
ECS is a more foundational way of organising code, in the same space as OOP itself.
One of OOP features is encapsulation: data and methods living together. ECS enforces the complete opposite: complete separation. Another OOP feature is inheritance. ECS removes inheritance and replaces with composition.
If one says that ECS a just another design pattern, then it's only fair to say that OOP is also just a design pattern in itself, since I could implement OOP itself in C (see Linux Kernel), Haskell or ECS if I wanted.
Well, it is?
What do you mean by looping itself? How is having a struct dissimilar to having an object iterated, especially with C++ essentially having barely any difference between a struct and a class?
If you say so we can agree on that, yes. :)
> What do you mean by looping itself?
I mean that that ECS is not merely looping over an array like GP claimed. In production ECS, Systems normally act on more than one kind of Component, so "iterating over the array" of components is not feasible. Iterating over entities kinda kill the performance and data locality too, so you need something smart closer to "materialised SQL views" to get performance.
> How is having a struct dissimilar to having an object iterated, especially with C++ essentially having barely any difference between a struct and a class?
I never said it was. I just said that merely using a "class" doesn't make something an OOP pattern, especially if the implementor has no choice over which programming feature to use.
Not really. Components are long-lived so they're not allocated on the stack. And contiguous arrays of structs/objects is the best case for cache locality, that's the entire performance case of ECS.
This is also where ECS as a pattern falls down (from the simplistic ideas about them being performant) because keeping components in packed arrays is only one part of making them efficient. The biggest issue is making sure your entity data is best organised for the most common data access patterns. For example you can’t actually store data in big blocks of single component type but need to consider the shapes/archetypes (common sets of components) that make up an Entity. For each archetype we can store the components packed in arrays so that they can be iterated contiguously. That way Systems can visit all of the archetypes that contain the components they care about and iterate straight through them whilst minimising cache misses. This approach obviously falls down the more heterogeneous your Entities are. It also incurs a cost when a component is added or removed from an Entity so makes some common ECS patterns more expensive. There are other approaches with different trade offs.
But this is also pretty academic for most games which don’t have the entity count or gameplay complexity to actually require this level of performance anxiety.
The advantage of ECS in this case is that it enforces data organisation to be coherent to the way data is used.
All this is mentioned on the seminal articles about Data-oriented Design, including the ones that popularised the terms AoS and SoA, and in lots of literature about modern ECS.
I suggest looking up what Entities and Components mean in ECS. It is doing all those things you're thinking of, but in a more optimal way.
Am I missing something? Sort your data structure and quickly loop over them? Why would you ever do things at n^4 complexity?
> then it's only fair to say that OOP is also just a design pattern
Yup. It is. No reason to treat it like a religion throw it away because you can't force
Yeah. You're missing the part where I said ECS is not just looping. It's clearly also not just sorting, if you know how ECS is structured. You need either indexing, like databases, or you need to watch for changes and cache them before usage, like in SQL materialised views. If you want to keep discussing the basics of ECS I strongly suggest searching at least how it is structured, its goals, and how it works.
My understanding is any ECS implementation would always spend the time bookkeeping to prevent O(n^4) or other combinatoric explosion. Systems run every frame while entities are spawned much less often, if at all between load screens.
Nested linear searches as you suggest would also blow out any locality improvements, which made the suggestion even more confusing to me.
Unity's implementation is deeply focused on keeping component data tightly packed so systems can quickly loop over them with SIMD, or even batch them across other threads. They keep entities sorted in buckets by component list, eg entities with components A, B, and AB are grouped separately (but still as arrays of the underlying component data).
They can trivially pass organized sets of arrays to any system that may need them. No linear searching is required as the arrays of separate component data are naturally matched. If a system needs to operate on A and B, the paired arrays are ready made.
...but what do they know?
It is in keeping with the spirit of OOP to wrap up different entities' data in separate objects and have them interact with each other through methods. Exposing all of every entity's data to the world and letting unaligned actors unilaterally mutate many entities' state with no middle man is very un-OO, even if you dress it up in classes and methods.
But that is NOT the pattern. Entities are ids, they have component tags that opt entities into some functionality and the systems encapsulate and track the mutable state that system controls.
If you want to start sharing ownership of mutable state, that basically comes down to an implementation detail. Unity's ECS system does NOT let "unaligned actors unilaterally mutate many entities' state with no middle man." One could perhaps implement it that way if they so choose. It's just a matter of taste because, again, the design pattern is not inherently OO or non-OO.
Unity, for example, encapsulates the actual component data and uses a fairly complex API around command buffers to schedule readonly and read/write commands across many threads.
I think the author was on the right track in recognizing OO as "a set of mechanics we can cherry pick," but I really don't follow how they think an embrace of ECS style would force programmers to abandon most or all OO mechanics.
Shouldn't Unity's DOTS/Burst subset of C# count? It gets optimization from alias analysis that you can't get with an ECS in C++ or unrestricted C#.
https://docs.unity3d.com/Packages/com.unity.burst@1.3/manual...
Is Smalltalk in 1972 an OOP programming language?
Also, nesting of complex types (i.e. making assumptions about the shape of the domain model) also seems to be a large part of what causes pain in larger code bases.
> Most programmers I have met in person tend to divide the world thus:
> Wacky research stuff. Unproven, Scary, Unmaintainable.
For us, the "Wacky research stuff" is how we finally solved many frustrations we had with implementing software against our particular business. A relational/functional hybrid model (implemented using C#8 & SQL) has proven to be far superior to any other approach we have taken over the last 6 years. But, we still do OOP in other places where it still makes sense to do so.
Actors have been automatized up to a unique isomorphism.
See the following:
Typeclasses allow you to treat your entities as objects with behavior, while at the same time separating concerns in your codebase. The behavior is defined wherever makes sense according to the separation of concerns and then "added" to the bare entity object where it is needed.
You don't really even need typeclasses to achieve this behavior. It's just the adapter pattern.
You might not get the absolute best efficiency this way, but it's not a fatal blow to combining ECS style and OO style.
Compare that the "functional programming" which in comparison has a glas clear definition, which makes it much easier to decide if some program is "pure functional" or not, avoiding prolonged and unproductive discussions.
Does it?
Human language is unsuitable for "glass clear", unambiguous definitions, just because of its inherent fuzziness (which is nota bene an essential property for efficient communication).
Not really. FP has an obvious connection to lambda calculus. Many OOP languages don't even have a straightforward notion of "function", which is what lambda calculus is all about.
> There is a well-established theory of functions, the λ-calculus, ... However, our theory of objects is self-contained; it is the first that does not require explicit reference to functions or procedures.
So that answers your question in the negative and proves my point: no, not every language is based on lambda calculus.
See e.g. Landin, P. J. (1965): A Correspondence between ALGOL 60 and Church's Lambda-notation.
No.
Prolog et. al. is based on Horn Clause representation and evaluated using something called SLD resolution.
"Concatinative" languages (like Joy) are based on function composition (not application.)
(FWIW, the Turing machine is not based on Lambda calculus. Not that TMs are a programming language.)
I sometimes wonder about the possible form of the "Platonic Ideal" is that each of these systems represent. George Spencer-Brown's Laws of Form seems to me to be the ultimate concrete example, but that's not a universally held opinion. :)
It's like Roman numerals vs. base-10 numerals, eh? "XXIII" and "23" both denote the same number (which may or may not exist depending on your metaphysical outlook) but neither notation can be said to be "the" notation.
let x = e
y = e
in ...
is not equivalent to let x = e
y = x
in ...
if your semantics can distinguish expressions by the time they take to evaluate.Still (to address your original question) people do consider lambda calculi with effects, so if the expression-based fragment of an imperative language supports lambdas with lexical scoping then sure you could get away with saying that "their expression are based on lambda calculus".
So taking eg. Template Haskell, you actually loose referential transparency since then even the syntax itself matters and you can’t just replace it to an equivalent expression. I have no experience with Template Haskell itself so bare with me, but eg. having a macro m that converts the constant 2 to the string “two” while others to empty string will fail to be referentially transparent since `m 2` will not be the same as `let a = 2 in m a`.
let a = 2 in $(m a)
and since `a` is not in scope at the time the TH splice runs compilation will fail. Secondly, even if you could write it, I don't really think something that contains Template Haskell should be called a "Haskell expression".With regard to pron's comment, he has a long history of being technically correct with regard to Haskell. The operative point is
> What they mean is that the language is referentially transparent (like most language) and a term's reference (denotation) is an object value in the language
i.e. Haskell is referentially transparent with respect to a particularly coarse semantics (value semantics).
Anyway, your greater point that referential transparency doesn't imply immutability is completely correct.
It's a programming paradigm based on simple, well-understood mathematical framework. It's also the starting point for most research in programming language theory.
“A closure can capture constants and variables from the surrounding context in which it’s defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists.”
https://docs.swift.org/swift-book/LanguageGuide/Closures.htm...
What makes you think Swift only supports functional programming?
> I would have used Haskell doc reference
And if you would have, you would have disproved your own point. In Haskell, closures cannot mutate state, because mutable state is not part of the language semantics. It's modeled explicitly via monads.
I'll deliberately avoid terms so we don't get to argue about words, but rather substance.
1. Objects have identity that's independent of their state (two objects with same state are still distinct).
2. Objects contain implementation which is hidden, including optionally mutable state.
3. Communication occurs through handlers/methods/calls, which provide only INDIRECT access to the object's internal state.
4. Objects can stand for one another, if their handlers react in a compatible way with the object they're replacing.
I tried to keep it as basic as possible, while avoiding topics about inheritance and what not.
Now the question is "do you need an OOP language to do OOP", and no, you don't. There's overlap. For example if you take the above, you'll notice that closures in FP match all of those except one thing: mutable state. So if your FP language allows mutable state, then you can do OOP in it with closures. And that's not a proof that OOP is not a thing, rather it's a proof that what some small minds keep putting in opposition, things like FP vs OOP vs DOP are just pieces of a larger puzzle that forms a single cohesive picture.
It may seem like a weak criteria if you're using to OOP/FP languages that provide typesafe, efficient polymorphism out of the box. But the reason we have Obj C and C++ is that polymorphism (while also maintaining encapsulation etc.) is a living nightmare in a language like C.
Immutability, first class functions, laziness are all such tools. And which tools are used can vary, just like the tools that help "doing OO" can vary.
But I think that FP does have that core which has a glass clear definition. I'm not sure which is the core of OO. Maybe it's the idea of "encapsulating" mutation (basically inverting the core of FP). Maybe it's the principle of virtual methods / messaging. It's not so clear I think.
> Some FP is lazy, some isn't.
The definition of FP implies that laziness is irrelevant. A language can enforce a pure functional style without being lazy or not.
> Some is immutable, some isn't
That's wrong. And it is a good example of why FP is much clearer defined. Referential transparency implies immutability. So there is no pure FP code that uses mutability.
> Some claim it's a style in any language, others that it's a type of language.
It's neither. It is a property of a program. Some languages enforce this property, others make it impossible to write a full program in this style. But that's independent of the definition itself.
I hope that explanation sheds a bit of light into it.
Most real-world programming requires handling of side effects somewhere, so such programs have parts which are not referentially transparent.
It's why I distinguished between "core" and "meat" in my other response. Functional programming languages offer features that help making a larger part of the program referentially transparent (and, optionally, to ensure that property in the types).
> Nowadays the word "functional" is used different from the original meaning (which some now call "pure functional")
I don't think the meaning changed, just the tools offered and the scope of the support they offer to achieve referential transparency (pure functions). Early Lisp offered expression oriented programming which was a step towards it, but used dynamic scoping which isn't really helping. Later on both Lisps and ML offered lexical scoping. Then the purely functional trend came up via Miranda and (Clean and) Haskell, the latter encoding purity in the type system. In the last decade mainstream programming languages added functional programming features but are falling short in encoding purity in the types (just as Lisp never did), which means that it's (easily) possible for the abstractions offered (both functions but also libraries built on iterators) to not be referentially transparent, manual care (and/or culture) is required instead. The tooling doesn't help enough to guarantee referential transparency. But the core idea, the target of the efforts, is the same.
> Referential transparency implies immutability.
I didn't go into immutability in my other response because you can use mutation inside pure functions, as long as the effect doesn't leave the function. Building referentially transparent functions does not imply using immutable data structures or variable bindings inside.
That's not correct though. Look at Haskell (or even stricter Idris). This is real world application code that can do whatever Java, C++, ... can do, but it is pure functional and hence doesn't contain any non referentially transparent parts. Just because an application is written in a pure functional style does not mean that it cannot e.g. write into a database when it is executed.
Of course many languages don't even allow to use a fully pure functional style. In that case, a more or less big functional "core" is the closest one can come to a pure FP style.
> I didn't go into immutability in my other response because you can use mutation inside pure functions, as long as the effect doesn't leave the function.
Sure you can do that and sometimes it is even the best practical solution. And the function might even be referential transparent - but your whole application is now not pure functional anymore, because a part of it is not referential transparent anymore.
Hence: if an application has to satisfy the criteria of being "pure functional" then every expression must be referential transparent and hence no mutation can be used.
If you use the state monad, you encode local side effects. Then you use runState in a function that has a non-monadic type--a pure function.
Feel free to argue that the state monad is only having pure expressions because it can be represented using only pure functions and partial evaluation. For all practical purposes it is used to write code that exerts (local) side effects into the state maintained by the state monad.
You can a function in say, OCaml, that fills an array via mutation using only function arguments to deduct the contents, and then returns that array. That function is pure (it exerts no side effects outside and is not exposed to side effects from the outside, its result value only depends on the function argument values). Yet how is that not using mutation, non-referentially transparent parts inside?
You can write the same function with a mutable vector in Haskell (Data.Vector.Mutable, using the ST monad). How is it not exactly the same code as the one in OCaml except for the types that guarantee the purity? Yes, it may be using pure expressions underneath to guide the type system to enable the proof of purity. But does that change what I said? Both to the programmer and (after optimization) to the machine the code is using mutation. You can model mutation using only pure functions. That may be useful in some ways, but you can also take a C program, represent every memory address as an entry in a tree of immutable nodes, the outside world as an abstract entity with versions, and recreate it with only pure expressions. That may be what is useful in some contexts for developing proofs or to implement type systems, but is this relevant for a programmer interested in writing more functional programs? Why not just say that using ST is using local mutation (that the type system can guarantee will not leak outside) and be done with it?
x = expression
y = expression
...
is the same as x = expression
y = x
...
and that, secondly x = expression
...
is the same as ...
when x does not appear in ... . Those are the important properties of Haskell (with regard to this discussion). Whether this advances the discussion is another matter ...I would say pure FP languages such as Haskell are actually the best for such a case. Because in these languages the order/timing of lines of code are decoupled from the execution order/timing.
... That's good in your opinion?
But when the percentage of concurrent code grows bigger, then it is better - because you lose the LoC<->execution relation anyways.
You can for example see that in Python. Python makes it really hard to break out of that model and have a line of code running while another one is running. That's why paralle and async/concurrent programming is such a PITA in python.
So, in my opinion for some problems the FP style is much better, because instead of "working your way around", you embrace the fact that LoC and execution are inherently decoupled.
It's what the current discussion is about, since I explained why I explicitly didn't make a statement about immutability being required for FP.
> The operative point is that, in Haskell, firstly
> x = expression
> y = expression
> ...
> is the same as
> x = expression
> y = x
> ...
Sure but that's the pure part of a program.
x <- expression
y <- expression
is not generally the same as x <- expression
let y = x in ...
and I pointed out that you can have such a program (i.e. with mutability) inside a function that is pure to the outside.The interesting feature of Haskell / Monads is that you can have mutability be both inside and around pure expressions (impure code making use of pure code and then impure code again inside the implementation of pure code). Not everything in a Haskell program needs to be pure. Of course, pure expressions are good since they are easier to reason about. Also, they are the core idea of FP as I mentioned earlier. But, I want to avoid people taking away the impression that you then never can use mutable data structures, and that's why in my original comment I didn't make a statement about immutability being required for functional programming.
Functional programming has a core idea and that is pure expressions (referential transparency), and then features to help increase the area of programs which are pure expressions and increase the confidence / safety of them being so. With regards to that confidence, Haskell goes far (there's still unsafePerformIO (and non-total functions) so it's not completely perfect). And I'm not disputing that when I say that it still is and always was called functional programming when there were no language-provided features to give that confidence, or only "half-assed" ones if one wants to call them that. Or only culture and convention. My point in the whole discussion is to explain why functional programming doesn't mean the same thing for everyone, and how it can both be that @slver says "there's no clear definition of FP" and that there is at the same time. There is a clear definition for referential transparency, which is the core idea of FP. But the term FP also encompasses various approaches and tools with varying qualities to enable that core idea, and it's here where there's no clear definition.
Yes, but as long as this is done with only pure expressions, then the whole program is pure functional - that is what the definition is about.
> You can a function in say, OCaml, that fills an array via mutation using only function arguments to deduct the contents, and then returns that array. That function is pure (it exerts no side effects outside and is not exposed to side effects from the outside, its result value only depends on the function argument values). Yet how is that not using mutation, non-referentially transparent parts inside?
The guarantee or at least convention of referential transparency for every expression has benefits. It means I as a developer can make certain assumptions about code that I could not make otherwise. I can do certain refactorings and changes while being sure that it will not change the program semantics.
As soon as parts of the program is not pure anymore, I know have to always re-assure myself that I'm currently working with the pure part of the code. Same for any tooling that I build.
As I said, you can make that choice and sometimes it is the best thing to do - but you cannot call your application "pure functional" anymore when you do.
> That's wrong. And it is a good example of why FP is much clearer defined.
OCaml and F# are two examples of functional languages with freely supported mutable data types. So maybe what I said is "wrong" about your personal idea of what FP is.
But it's clearly not "wrong" about the fact some people see immutability as optional for FP, which is the very point I'm making.
Both OCaml and F# are multi-paradigm languages that have FP features, but are not restricted to FP. The support for mutability comes from their imperative/OOP ancestors, not from FP.
See the top answer here: https://stackoverflow.com/questions/210835/what-is-referenti...
The problem is even called out in the answer itself:
> For example, our example "Edinburgh has been the capital of Scotland since 1999" signifies the fact that "capital of Scotland" depends on the time at which it is being considered. Such context-dependence is a reality, both in natural languages and programming languages.
"Therein lies the rub." Such context-dependence doesn't have to be a reality in programming languages. "2 + 2 = 4" is an eternal verity, not subject to context-dependence. The whole point of FP (going back to Backus' Turing Award paper where he introduces and defines "FP") is to operate in the pure realm of [binary Boolean] logic.
The latter is (quoting pron’s older comment: https://elarib.com/item?id=22141647 ): “an expression e is referentially transparent if any sub-term in it can be replaced with any other having the same reference (aka denotation) without changing e's reference”. Perhaps I should have linked this comment over the stackoverflow answer, as it also mentions how eg. template haskell or LISPs are actually NOT necessarily referentially transparent due to macros. So while in Java replacing the constant 2 with a variable having value 2 is always equivalent (in reference, not necessarily effect!), the same may not be true with a given macro applied.
Could you point out a specific point in my answer that is not compatible / or contradcits with the common definition of referential transparency?
> what you mean is simply side-effect freeness
This really makes it sound like an assumption. And:
> So just exchanging ref. transparency with “not having side effects” in your otherwise perfectly fine and interesting reply will make it correct
You implying my answer is incorrect here but you are not giving an example of what is wrong even though I asked for it.
That being said, I believe that the definition of referential transparency (and therefore FP) is not useful at all without making "semantics" a part of it. And this is the point where discussion can happen. For example, is the performance characteristics of a function part of the program semantics? I would say no. Even though it matters in practice. Same goes for logging: one can argue that logging (that can not blow up due to e.g. lack of disk space) can be considered irrelevant for program semantics and hence a function that logs something with a "print" can still be pure.
If you remove semantics from the definition then the definition becomes rather useless imho. In that case I would rather change its meaning or switch to a different definition alltogether.
And I agree, we do have to specify what is the denotation of an expression that shall remain constant after replacing a sub-expression with that part's denotation -- for example choosing value of expression after evaluation is not too interesting, as it is shared by the majority of languages, and can only be "violated" by macros for example. Choosing the denotation as "value and side-effects" doesn't add much over saying simply that the language is pure and as you also note, purity has different levels.
> Choosing the denotation as "value and side-effects" doesn't add much over saying simply that the language is pure
You are aware that, regarding the discussion between us two, it was you and not me who even used the term "side-effect" first, right?
https://news.ycombinator.com/item?id=27414992
> FP means that every expression (= part that can be evaluated) is referential transparent
> Referential transparency implies immutability
Both of these statements are false. As I said, Java is referentially transparent.
addOne(2)’s value is the same as the value of the last line in `var a = 2; addOne(a)`. And Java is most definitely not a pure FP language, and its referential transparency doesn’t imply immutability. You can argue that inserting `int two() { sideEffect(); return 2; }` ‘s invocation in place of 2 will change the semantics of the program but that is precisely the meaning of a side-effecting function — so using referential transparency is redundant in that meaning.
I disagree with you - I think you are using a different definition and there seems indeed to be discussion about what the definition really is. Wikipedia for examples uses the definition that I use, but in the Talk-section someone made a similar complaint to yours (or was that you :)). [https://en.wikipedia.org/wiki/Referential_transparency]
I stick to my definition though, because... I find it more useful and I think it is the more common one.
Two decades later we're still trying to clean up the resulting mess, but I think all in all we're on the right trajectory since around 2010.
> Two decades later we're still trying to clean up the resulting mess, but I think all in all we're on the right trajectory since around 2010.
And no, we didn’t use to write better code, it is only bias/we simply ignored most of the requirements, eg. ASCII-only, etc. Unfortunately most software products are not incentivized to be over the top stable so a few bugs here and there are “okay”. But that is an orthogonal problem.
Objects originate in Simula-67.
OOP derived from objects of Simula by its very name.
Simula does not have messages.
Instead Simula has virtual procedures attached to classes
such that each object is an instance of a class.
Furthermore, Simula does not have interfaces.
Actors originated in 1973 as the universal abstraction for
digital computation including concurrency, which was omitted
from Simula and subsequent OOP programming languages.
Furthermore, Actors differ from OOP objects in that they are
rigorously characterized up to a unique isomorphism.
Actors are computationally more powerful than the lambda
calculus and Turing Machines.
Entity/component/system sounds like model/view/controller.
But I like to think of data/logic/presentation. The point being that keeping these dimensions orthogonal is more imoprtant than how they are labeled.
At it's barest, a controller organizes models and renders them into a view.
Again simplifying, an entity has components which describe it's functionality, and the entity can get passed through a system. The entity is the concept of the thing, and the components are it's features. The system uses the features to make decisions about how the entity interacts with the system.
I've never seen MVC used in anything but single-run applications that end with an discrete output. Whereas ECS is more often used in software with a perpetual state and loop, such as game engines.
Exceptions abound.
MVCs original use was GUIs, which are not “single run applications with a discrete output”, but instead “software with a perpetual state and loop”. Yes, its since been adapted to web use, which usually fits that single-run description, but still...
In MVC a typical interaction goes like this:
-> INPUT: user interacts with view
-> view triggers controller action
-> controller updates the model
-> controller passes model to view
-> OUTPUT: view changes
-> (rinse and repeat)
The important part here is that there is a single input that flows through the steps and generates output. This maps well to web frameworks where the input is an HTTP Request and the output is the Response. It also maps well to GUI frameworks where the input is a user interaction event and the output is changes to UI on screen. The perpetual "state and loop" for triggering the next input from the previous output is implementation detail and doesn't really matter for the pattern.ECS is different because it doesn't have discrete interactions converting input->output. Instead they are built up out of a number of systems all interacting with eachother. User inputs enter a system and that triggers a complex flow of other system interactions, eventually generating 0..N outputs.
Disclaimer: I have lots of experience using MVC in various domains, not that much with ECS.
Wut? Every desktop GUI framework in widespread use is some variant of MVC (i.e. MVC, MVVM, MVP, whatever…).
Maybe to someone who has no idea what ECS is. ECS is not three things named "entity", "component" and "system".
Instead you have entities separated into components which are transformed by one or more systems that operate on all of them in bulk.
There's literally no overlap between ECS and MVC.
> But I like to think of data/logic/presentation.
It's OK to think however you find it useful for yourself. But data/logic/presentation isn't MVC either.
If I have to use these terms, it'd be rather:
- model: data+logic
- view: presentation output processing
- controller: presentation input processing & mapping to model(s).
My point regarding orthogonality is that lif is better when the state is pure. Just the database table and its rows.
The logic functions operating on that state are better kept to map, filter, and fold as much as possible.