As the article mentioned the meaning of OOP changed over time. Alan Kay has a description of OOP that sounds like the actor model used in erlang/elixir or mq and consuming services elsewhere.
Probably yes, if you ask Dr. Kay, probably rather no, if you ask other programming language authors. It e.g. lacks inheritance and some other OO features present e.g. in Smalltalk-80. But I guess the question was rethorical anyway.
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.
The article doesn't argue that ECS is a good fit for everything. It argues that it is a better fit than OOP for some of the things OOP was intended for. And that it's a broader theme, where OOP is usually not the right tool for the job, even when it was originally intended to solve that particular job.
The article is primarily trying to suggest that "OOP" has "died" so many times that it should no longer be used. As if OOP was an ideology to be committed to or not.
Not really. An "Entity" in most modern ECS implementations is normally implemented as an integer, which is used as a handle to which multiple components refer to.
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.
All engineering and technical tasks really. I was taught to think of learning these techniques as putting tools in my toolbox. Then when a situation arises I can select which tool I think might be the best fit. No ideology, no cargo-culting, just get it done.
Regarding lambdas and closures, it's not really a blow to OOP - it's simply another interpretation of methods and fields. As the famous koan[0] said, "objects are just a poor man's closures".
The article clearly shows that using the term OOP without further explanation is not helpful at all, rather confusing, because everyone has a different understanding.
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.
I would say it doesn't have a formal 100% unambiguous definition, but it's certainly _more_ well-defined than OOP: a functional programming language is one which is based on lambda calculus.
Isn't every existing programming language eventually based on (or at least traceable to) lambda calculus? OOP is just another way to organize code.
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).
> Isn't every existing programming language eventually based on (or at least traceable to) lambda calculus?
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.
Yes, that's one of the most prominent in a long line of attempts at creating formal models of OOP. And in the preface, you'll find:
> 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.
There are also different "schools of thought" for functional programming. That in itself is not a bad thing. The thread was about formal definitions; such are therefore available for those who need them.
They're equivalent, but not identical.
Lots of formal systems are Turing complete (e.g. Wang Tiles) and so can compute the same results as each other. Wolfram goes on about this some.
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. :)
Of course, but you could do that with the other formal systems too.
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.
That's ok; my concern was just whether "every existing programming language eventually based on (or at least traceable to) lambda calculus" responding to the fellow which claimed that "a functional programming language is one which is based on lambda calculus".
How are expressions not based on lambda calculus in most “ordinary” languages? Yeah, they may side effect but at the same time lambda calculus itself is untyped, so is Haskell all that much closer to it over C++?
Many other languages are not expression based at all. for loops cannot be considered "lambda calculus", for example (although of course there is a translation of them into a lambda calculus equivalent).
While they do have statements as well, they usually also have expressions, which are referentially transparent (and yeah I know it’s not the usage most people use, but it is the correct one: https://stackoverflow.com/questions/210835/what-is-referenti... ). (They don’t employ currying though)
I never quite understood Uday's objection. Any language can be referentially transparent if you consider it's denotation to be just its syntax. Then you can always replace equals with equals, but the only thing equal to an expression is the exact same sequence of characters! The whole point is to be referentially transparent with respect to as coarse a semantics as possible. Haskell gets some of the way there, yet even
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".
I’m having a “debate” about it at another thread as well, so let me just point to a perhaps better explanation: https://elarib.com/item?id=22141647
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`.
Firstly, I'm not sure you can even write that in Template Haskell. The syntax would actually be
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.
If I understood you correctly, you say that functional programming is what people usually call "pure fonctional programming"? If that's the case, the definition is better than OOP, but I don't think it's a property of functional programming but just that OOP is talked about more by more diverse people.
Yeah - the term "functional programming" is nowadays as unclear as OOP. But "pure functional programming" is not - there is no equivalent in the OOP world.
The funny thing is it's exactly the same thing with functional languages, and no, there's no clear definition of FP. Some FP is lazy, some isn't. Some is immutable, some isn't. Some claim it's a style in any language, others that it's a type of language.
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.
From the Swift docs regarding closures (I would have used Haskell doc reference, but the Haskell docs are so esoteric and vague as to be nearly useless):
“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.”
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.
Whether closures are or are not allowed to mutate state depends on the programming language (or the aim of the programmer, if the programming language doesn't force their way). Closures are a syntactical building block helping functional programming (building functions out of objects is just much more syntactical overhead), but whether a particular closure is a pure function or not is a separate concern. If the function is to be pure (hence satisfying the core of the "framework") then it would not be allowed to mutate state.
It's true that functional programming also doesn't have a formal definition, but I wouldn't say it's "exactly the same" as with OOP. Functional programming is based on a simple well-understood mathematical model: lambda calculus. While there are different flavors of lambda calculus, they are related in obvious ways and one can reach formal conclusions them as a whole, e.g., the Church-Rosser theorem. In contrast, the many attempts to formalize the essence of OOP have resulted in countless wildly different results, so it's impossible to say anything meaningful about OOP, with the possible exception of this sentence.
To all the people downvoting me: if you really think OOP is as well understood as FP, learn some programming language theory. Tell me what the universally agreed foundation of OOP is. You won't be able to, but I can tell for FP it's lambda calculus.
I agree that functional programming has a more widely agreed-upon foundational set of definitions than OOP. However, it doesn't follow from that that "it's impossible to say anything meaningful about OOP".
> Tell me what the universally agreed foundation of OOP is.
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.
1 is about object identity. 4 is about object type/function. I.e. polymorphism.
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.
AFAICT the core of FP is referential transparency. The "meat" of FP are the tools available which allow to (comfortably) increase the portion of the program that is referentially transparent.
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.
FP means that every expression (= part that can be evaluated) is referential transparent. Nowadays the word "functional" is used different from the original meaning (which some now call "pure functional")
> 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.
> FP means that every expression (= part that can be evaluated) is referential transparent.
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.
> Most real-world programming requires handling of side effects somewhere, so such programs have parts which are not referentially transparent.
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.
It's funny that you defend your view using Haskell, as I was replying with Haskell in mind.
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?
Equivocation about purity, referential transparency, mutability, side effects etc. is not very interesting. The operative point is that, in Haskell, firstly
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'm not deep into Haskell, but isn't the interpreter monad pattern effectively a language within the language that acts in a very conventional procedural way for interacting with IO and other external concerns? I just wonder... does Haskell really need syntactic segregation for this, and how is that better than any language which can declare/enforce pure functions/modules without the entire language being like that by default.
Well, give it a go! Or alternatively, look for languages that have been successful in enforcing any kind of purity and that also permit unrestricted use of higher order functions.
It's very hard to make Haskell look attractive to me, I deal with open systems (i.e. systems which interact with many other heterogenous systems which evolve in time), and Haskell is optimized for precisely the opposite case (a walled garden built out of elaborate types for verifying correctness). Haskell is the weird introvert guy in an extrovert world.
Interesting - for me it is the opposite. Non-functional languages usually work better if there is no concurrency involved. And 3rd party systems always introduce a certain amount of concurrency, sometimes more, sometimes less.
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.
> 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.
If your program is simple and LoC are executed one by one and that is totally fine, then FP just takes away the natural relation between LoC and execution and adds another layer of unnecessary complexity.
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.
Hmm, not really. See, for example, Clash, a Haskell compiler for synthesising hardware circuits, or Cardano, a massively distributed software system realising a cryptocurrency.
> Equivocation about purity, referential transparency, mutability, side effects etc. is not very interesting.
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.
> For all practical purposes it is used to write code that exerts (local) side effects into the state maintained by the state monad.
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.
> OCaml and F# are two examples of functional languages
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.
That answer is so informative and yet it completely misses the point.
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.
I agree with you, but this property of FP languages is called pureness/not-having side effects, not referential transparency.
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.
Fair enough. (I'm getting old, it hurts my head to imagine pure-but-not-referentially-transparent language; however, imagining referentially-transparent impure language doesn't.)
I didn’t mean to assume anything, I just wanted to note that the common definition used in FP circles is not correct. So just exchanging ref. transparency with “not having side effects” in your otherwise perfectly fine and interesting reply will make it correct — since for example Java is also referentially transparent which I guess you would not consider being a pure FP language. (Exchanging a value with a variable referring to the same value will not change the value denoted by the expression for example neither in Java, but it is not too interesting a property in itself)
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.
I didn't want to repeat myself as the exact meaning was debated in two threads in this post already, for more details please have a look at them.
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.
If you do not point out any contradictions even after being asked twice then I have to conclude that you simply cannot. You could have also referred specifically or quoted the important lines from the other threads, but you did neither. That's not helpful in a discussion.
> 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?
> 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.
They are quite different concepts and certainly in practice.
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.
> I've never seen MVC used in anything but single-run applications that end with an discrete output.
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...
That's interesting, I knew it was a pattern older than it's use in the web but hadn't given it much thought. There's certainly nothing inherent in either design that limits them to either use case.
Single run VS state+loop is not the best description of the distinction between MVC and ECS. I look at it like this:
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.
Classes were never definitional for OO, so prototypes, as originated in Self and adopted in JS, are not a violation of it.
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.
The essence of OOP is not inheritance, that’s a nice to have. The essence of OOP is to encapsulate the data and the functions that operate on that data into an object and hide the internal state of the object from the rest of the program.
If this is the essence of OOP, would you consider modules/existential-types in StandardML to be OOP? As a followup, what about OCaml (is that "more OOP"?)
See, that's just ridiculous and shows how undefined and vague "OO" is. The only relevant feature there is encapsulation, that most languages provide in some way. How is that OO?
I vote for just not using this term anymore and talk about class based langusges or prototype languages or whatever.
The problem isn't that the term is poorly defined. It's that it's widely misused. Dynamic dispatch (basically "inheritance") is what most experts claim to be the defining feature necessary to claim OO.
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);
};
Every OO proponent seems to have their own definition of OO. Another I’ve heard is “message passing”, although that one even has many different flavors since it isn’t well defined and some argue that methods constitute message passing and others envision something closer to an Actor model.
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”.
The concept of a message is older than the actor model, so I don't think it can be considered a fundamental part of the definition of a message (so Smalltalk-style messages are still messages). I do think the Actor model is the most ideal form of message-passing in a single-process system, and seems to be the direction many languages are heading toward as they evolve.
To be clear, I’m saying “something like the actor model”, not the actor model specifically, though I assume the actor model also satisfies even the narrower definitions of message passing.
> The concept of a message is older than the actor model
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.
For yet another definition, OO is about encapsulated polymorphic interfaces. Which raises a communication difficulty: encapsulated polymorphic interfaces are very useful in some of the cases (ADTs, modules), and a misfeature for the other cases (data modeling). I have yet to see a conversation, especially an internet conversation, where participants reach a rough consensus on the contextualized upsides / downsides of a given concept. It's either 100% heaven or 100% hell.
Do you have any references with detailed analysis of this issue? How much one should emphasize concrete data types vs. interfaces seems like a key consideration in language design.
Use it where it's useful, use something else otherwise.
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.
> The essence of OOP is not inheritance, that’s a nice to have. The essence of OOP is to encapsulate the data and the functions that operate on that data into an object and hide the internal state of the object from the rest of the program.
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?
I would say that polymorphism open to extension is also important, and subtype / supertype hierarchy. It implies some kind of composition of interface types in a static language.
How is it a flaw? There are data-first processes where a LISP-style data “objects” are better, but there are many stateful objects, eg. a database connection where you very much don’t want anyone to access a flag with this specific meaning or IP or secrets, etc.
I'd say most of these attributes map pretty well to modern OO practices in popular languages, particularly Java, where late binding comes from widely used inversion of control frameworks like Spring. And the underlying mechanism for these is dynamic method dispatch, which I'd agree with Robert Martin is the single unifying requirement of OO.
What you describe is the essence of an abstract data type (ATD). An ADT can be easily implemented in a non-object oriented language like C. To my understanding the essence of object oriented programming is inheritance and polymorphism.
> Alan Kay named the idea, but stole it. So he doesn't get to impose his religion.
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.
Somebody else invented it. Kristen Nygaard, in particular. He identified the three elements.
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.
Kay is said to have coined the term "object-oriented", but his conception of object orientation differs considerably from that of Dahl and Nygaard as well as from all current OO languages. Dahl and Nygaard invented the first object-oriented programming language (Simula 67) but didn't use the term. And yes, active objects reacting to messages (events) passed to them were already part of Simula I (1962).
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.
He has been pretty clear in giving a lot of credit to the inventors of Simula, but that at the same his ideas derive more from Lisp. He even has a Quora answer where he explains that Lisp is the greatest language because he views it as a big shift in thinking.
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.
On some people, Alan Kay's territorial claims to others' ideas leave a bad impression. Nobody begrudges him (with Dan Ingalls) Smalltalk, which represents a serious material improvement over Lisp syntax. But it looks like sour grapes to complain about C++, a language designed by a grad student of Kristen Nygaard that found success in industrial applications.
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.
I've seen a couple of his talks and read some of his comments here and elsewhere. I've never seen any territorial claims, quite the opposite. Can you give me an example?
Indeed. He's said that Sutherland invented OOP (along with the other things Sutherland "didn't know was difficult"). Like most people, I guess, I found out about Sketchpad from Kay.
Kay has promoted many different definitions of "object-oriented", over the years, and many disjoint variations on Smalltalk's early history. It is probably best to stick to the very oldest interviews if you care to learn the real history.
I don't come here to bicker, but I would like to see an example of a "territorial claim" you mentioned. For now it seems that when questioned about specific examples to your claims, you revert to some more unrelated claims.
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.
If "preprocessor" means anything at all, it means a program that alters incoming text according to textual rules, without analyzing language semantics. The C preprocessor operates strictly on text patterns. Likewise Ratfor. Likewise m4.
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.
Of course, something using C as a target language can be a substantial compiler. But not everything that outputs C is in the special category of taking "C with classes" to "C without classes".
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.
Terminology has stabilized, since. Cpre parsed and analyzed C-with-classes code, and synthesized C code just like any of the later compilers, and Cfront.
> Classes were never definitional for OO, so prototypes, as originated in Self and adopted in JS, are not a violation of it.
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.
I like the use of the word "mold" here; I've not heard it applied to programming before. Manufactured objects are often very different from the prototype, but it's harder to differ substantially from the mold.
It's rather commonly used as a metaphor for classes (synonym: templates), I've seen that multiple times. More recently Urbit's Hoon language used the word for type-assertion functions [1], though I'm very sure that its choice is mostly due to its idiosyncratic naming convention where each class of words have the same number of letters.
Why do you consider inheritance definitional? Is it definitional by de facto, or is there an authoritative definition that you are using?
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?
Do you have a citation for "object-based" as a separate paradigm? The oldest reference I can find to that term is in the C2 Wiki, where it describes not a paradigm, but a pattern for approximating OO (including inheritance) in a procedural language without OO features[0]:
> 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.
Dahl and Nygaard originated and developed the concept. Inheritance was in their definition. You can invent other formalisms, but those should have different names if you are not actively seeking to confuse people.
Where do we see prototypal inheritance in nature? I can think of lots of examples of composition, but none of prototypes. The closest I can think of would be DNA, but even that would be better modeled as composition in my mind.
I see prototypal inheritance in communication. When I communicate that I fried an egg you automatically make a number of assumptions about the properties of that event. If we generally hold the same assumptions then I've transmitted the concept with high fidelity.
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.
Mitosis is the cellular equivalent of prototypal inheritance; the cell clones itself, and then you have two cells, each a copy of the original.
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.
> 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?
To clarify, I don't think OO is natural for all problems, and I don't think it is natural for all people either. But for many people, it is natural.
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.
> The reason OO is a powerful organizational too is because it mirrors how many people naturally think about the real world.
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.
On the other hand, when you make coffee you probably aren’t thinking about how to heat the water, measure the temperature, adjust the flow rate, etc. When you use the microwave you don’t think about cycling the magnetron to hit the power level appropriate for the settings or spin the turntable. When you drive a car, you accelerate, brake, steer, etc. without directly manipulating all of the parts & microcontrollers which respond to those control changes.
You just call coffeemaker.brew(), oven.defrost(“meat”, “500g”), or car.accelerate(). From that perspective the OOP model works pretty well.
Nobody was claiming that it was unique - if you reread the thread, I was just pointing out that the dismissal of OOP not fitting the way people thinking about the world wasn’t necessarily approaching it correctly.
No. Your own post your reply doesn't address any of GPs dismissals about OOP, it merely incorrectly attributes the advantages of abstraction itself ("you probably aren’t thinking about how to heat the water") to OOP.
In your first paragraph you have examples where you are indeed the actor. In the case of the coffee machine though, you are only the actor initiating an action (eg. the main method calling a given CoffeeMachine’s createCoffee method). And here is the great thing about OOP, you don’t have to know what exactly happens inside the machine, the way it heats up water, grinds coffee beans, etc, you only know what result you want and how to initiate an action, everything else is the responsibility of the machine.
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.
I agree; many OO models are built in a contorted way that does not reflect reality.
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.
> What is definitional? Inheritance, encapsulation, methods.
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.
It sounds a lot like he's trying to claim victory via semantic quibbling.
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.
> 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.
> 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.
> 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".
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.
> 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 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.
> 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.
(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").
> I'm very clearly not arguing that all OOP proponents are always acting in bad faith.
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.
ECS is often implemented in an OOP language. I know of no ECS languages.
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.
> 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).
I don't think ECS as such would really make a language on its own, but languages that use traits instead of inheritance would be approximately that. Both ECS and traits eschew using the inheritance-style "is-a" thinking. ECS goes for "has-a" thinking, and traits go for "acts-as" thinking. A few popular trait-based languages that would feel comfortable to someone familiar with ECS are Rust or Go.
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.
If the language in question has decent reflective capabilities, then I'd question why they don't use it. Perhaps they ran some benchmarks and found the language runtime performance lacking or maybe they are implementing the ECS pattern to the letter (e.g. using an integer to represent an "entity") as opposed to implementing ECS as a concept.
ECS can also be implemented in functional or imperative languages, and it often is.
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.
Why do critics think looping over an array is somehow incomprehensible in OO and must be replaced with inheritance or something? Most ECS implementations are in OO languages. In Unity/C#, those systems are objects those arrays holding component information are even objects!
Its really not a stretch for OOP. Its just a design pattern.
ECS is doing a bit more than looping over arrays, though. In modern production ECS systems there's often the case where a System acts over two or more Components is very common, and looping itself is not good enough for those cases (because you get O(n^2), O(n^3), O(n^4), etc, etc complexity), so it's nowhere near as simple as that.
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.
> 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?
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.
Thanks for the details! Though wouldn’t using arrays of structs kill the performance the same way as array of objects do (stack-allocated ones)? So, one would essentially have to have a SOA, or — the OOP abstraction: a class of arrays that enclose this hot loop of code, making the implementation opaque to a calling code.
> Though wouldn’t using arrays of structs kill the performance the same way as array of objects do (stack-allocated ones)?
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.
A simple struct/object having in order an int, a string pointer and another int having 3 different things happening to them can in some case be worse than having all the ints next to each other, etc. ISIISI… vs III…SSS…III..
So for cache locality the latter is the best option.
Best way to think about ECS is a generic pattern to help make things data oriented. It’s not necessarily going to be optimal though. It’s SoA over the Entity rather than individual struct members which might in a more tuned setup be more performant.
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.
Not really. There's no silver bullet when it comes to AoS or SoA. If you have a pair of integers, A and B, that are supposed to be always read/write together, the ABABAB of ECS beats AAA...BBB in terms of cache locality, hands down.
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.
> System acts over two or more Components is very common, and looping itself is not good enough for those cases (because you get O(n^2), O(n^3), O(n^4), etc, etc complexity), so it's nowhere near as simple as that.
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.
Perhaps I should because it seems like you simply listed ways to sort.
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.
Being written in an object-oriented language doesn't make code object-oriented. I could write Java using classes as (namespaces for stateless functions) XOR (structs with no methods), and while it'd still technically all be objects and methods, in practice it wouldn't embody the spirit of OOP at all.
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.
Functions grouped with data is inherently OOP. Even if you don't interact with a graph of objects in a class, you're still using OOP if you define methods on your class and use those methods to interact with fields.
A method on a class is just syntactic sugar for a function with an argument. It takes more than just "the presence of methods" for something to be OOP.
Isn't the grouping of methods with data a core fundamental principle of OOP? Even if it is syntactic sugar, Variables, Types, Classes, functions, method calls, structs are just syntactic sugar too. It just so happens that the syntactic sugar of grouping functions with data is OOP.
>Exposing all of every entity's data to the world and letting unaligned actors unilaterally mutate many entities' state
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.
Its a bit of an implementation detail I would say. If you use a single thread and explicitly order your systems then its probably possible to have fully accessible global state.
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 agree. I think the author leans too much on the supposed incompatibility of OO style and ECS style, which they explain only by linking to the Wikipedia page for object-relational impedance mismatch. I've worked in many systems that satisfy the author's description of ECS in their treatment of the central domain objects, and they were all built in OO languages and were thoroughly OO in the rest of the codebase.
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.
IMHO what actually killed OOP was the attempted industrialization of software development driven mainly by "Enterprise Java" (and similar failed ideas like UML). The promise was that with OOP it's possible to build complex software projects with cheap "unskilled labor", because OOP would provide the "framework" for managing complexity by plugging together very simple parts.
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.
First of all, OOP is most definitely not killed. Second, OOP is indeed (one of the idioms) what makes it possible to build complex software — even complex, pure C programs sort of do something OOP-like in the end.
> 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.
Ruby is interesting because everything is an object like smalltalk but matz was also directly inspired by emacs lisp. I think it shows that you don’t need to only live in one world despite the evangelists on both sides demanding purity.
In my experience, OOP works great until you run into a problem domain with circularly-dependent types that actually make sense as such to the business. Certainly, there are ways to manage this within the realm of OOP, but I strongly argue that these solutions are half-assed at best.
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.
Even the most fundamental types in smalltalk derivatives are circularly defined (using Ruby as an example: Object is an instance of Class, and Class inherits from Object). Circularity doesn't make this any less useful as a model, just more difficult to reason about. It's a tradeoff.
> The question is, how do you add those components together? One solution is to use mixins. This gives us some flexibility back, but we can go even further. See, mixins still tie the data of the component to the code that processes it. But when you think of it, some data, like the position of an object, can be useful for several systems (rendering, collision detection, AI…).
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.
So there's this thing which one might call an "object" which is fundamental to any discrete thinking we ever do and is, in fact, prerequisite for "concept." There are some bad ideas in what we call "OOP," but if you think have a reason you don't need a thing in programming, then good luck to you.
… 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
I don't know what he considered the mainstream -- perhaps not Smalltalk -- but the comment pre-dates Java. (I think his POP-2 pre-dates Smalltalk.)
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?
In terms of Smalltalk, the answer is "yes". Some examples of what it looks like include the original Self language / environment [1], Newspeak [2], and even Tony Garnock-Jones' implementation of Actors [3].
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.
219 comments
[ 3.7 ms ] story [ 248 ms ] threadIs Smalltalk in 1972 an OOP programming language?
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.
[0] https://blog.ploeh.dk/2018/01/22/function-isomorphisms/
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.
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`.
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?
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.
is not generally the same as 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.
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:
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.
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.
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?
https://papers.ssrn.com/abstract=3603021
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.
[1] https://en.wikipedia.org/wiki/Template_method_pattern
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...
[1] https://en.m.wikipedia.org/wiki/Object-based_language
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.
[1] https://urbit.org/docs/hoon/hoon-school/molds/
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.
[0] https://wiki.c2.com/?WhyObjectBasedProgramming
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?
[0] https://kotlinlang.org/docs/delegation.html
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.
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...
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.
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:
* https://papers.ssrn.com/abstract=3603021
* https://papers.ssrn.com/abstract=3418003
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.
… 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.
[1] https://selflanguage.org/
[2] https://newspeaklanguage.org/
[3] https://tonyg.github.io/squeak-actors/about.html
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.