Alan Kay on the meaning of "object-oriented programming" (2003)
notes.shixiangxi.com
notes.shixiangxi.com
The two main lineages of OOP are Simula and Smalltalk. But from what I recall, modern languages actually inherited more from the Simula side in practical terms, while the terminology and philosophy were influenced more by Smalltalk. [1]https://www.makonea.com/en-US/wiki/object-oriented-programmi...
> something like a small computer of its own
Which corresponds pretty well with the Simula I concept, published 1966 in the Communications of the ACM. A Simula event notice (time, process) in the sequencing set is just a message step(process, time) in a priority mailbox; the two are the same mathematical object, making Simula's discrete-event active processes and Kay's message-passing active objects trivially isomorphic.
He does research in "the history and philosophy of programming", amongst other things.
It's my favorite deep dive into the subject, it's super thoughtful and well structured.
I'd say games are a bit special, though. Smalltalk works pretty well in interactive applications - while it's true that encapsulation is an important part of the language, it also provides all the tools to pry objects open when needed. Out of the box, it's much better at that than C++. The problem is that video games demand very high performance, and Smalltalk is even slower than normal when you use reflection. If not for that, it's perfectly doable (if not very convenient) to treat a Smalltalk image as a soup of data with behavior attached. Casey also mentioned the focus on education in Alan Kay's projects: it's probably one of the reasons Smalltalk is largely devoid of modules, namespaces, packages, and even files with source code - things that are very useful when collaborating in teams, consuming external dependencies, etc. There are Smalltalk implementations that provide those, but they're either very niche or very niche and extremely expensive... And still slow.
Still, interactive software that doesn't have the performance requirements of games is definitely more convenient to write in Smalltalk than in C++! I have high hopes for GToolkit, I just need to port a few ideas from VisualWorks and figure out how to actually set up a project structure friendly to external agents (the GT agent harness is very nice, but it forces you to use an API, which is way too expensive for the results you get...)
It was originablly conceived as a simulation of a distributed system.
Distributed systems can be useful but does anyone really believe that they are simpler or easier to develop and maintain?
The amazing part to me is that so many were trained and convinced to accept that adopting this simulation could make all programming easier or somehow "better". As if adding complexity would magically lead to simplification.
I believe that programs written in languages made for distributed systems are simpler and easier to maintain.
Also I believe that one of the major problems in modern computing is that most languages we use do not understand that even trivial programs require ‘communication’ and OS/hardware facilities that have behaviours of distributed systems such as latency, transient faults, etc.
Graphical programming like LabVIEW looks very appealing. The product demo practically sells itself. Sure, it fits well for a very narrow class of use cases. But even fairly simple things in a textual language quickly become an unwieldy mess. (Try factoring an integer in it…)
There are formal models for distributed systems often solve the “easy” problems that didn’t need solving while making various practical concerns harder/impossible such as timeouts or node failure.
If you try to maintain a decentralized system, it will be hard yes.
However if you see the nirvana and try to maintain the parts of the system, it will be easier.
Dinosaur. Whale.
I think these were success stories for a long time. So, it is possible to develop and maintain complex systems.
I think the problem is that we can't really architect a program well until after it works and fulfills all of its design requirements. Those are usually in flux and then we have to engineer dimensions of extensibility to prevent committing to the wrong architecture in the beginning. Those extra dimensions of extensibility come at some cost too, as they often mean indirection and cognitive overhead.
We often don't write the same program twice, or even three times, unless it's a really important program or a we just like doing it. Contrast to other fields, e.g. building houses, where we have lots and lots of examples of how to do it and they started to form archetypes. We try to call those design patterns in programming, but they're actually pretty vague and low-level. Can you imagine going to build a house and thinking "yeah, I think I need to use the wood-and-nails design pattern here, and I think I'll do rafters-and-shingles there, with some brick-and-mortar there." Yeah, real specific ideas on how to do it.
Then it breaks down- depending on how deep your abstraction level goes (A car has 4 tires. 4 tires are made of: xyz). It also breaks down, with the abstraction in the real world above the modeler. Some think in organization, some in physics, some in tribalist constructs, some imagine a god object above them some don't. It was communicated as a tool using "shared" state as a communication helper.
But we do not share as much state as expected.
OOP is just another layer of philosophy which builds on top of that. It's more specific, imposes additional guardrails. It requires objects with state encapsulation (locality) and message-passing as the mechanism for components to interact with each other; each component is responsible for changing a subset of the state of the system. Each component is responsible for handling messages (calls to action) by performing local state changes and potentially also sending messages to other components which have more specific sub-responsibilities.
OOP without "High cohesion, loose coupling" is almost worthless IMO. It must build on top.
I think were most people fail with OOP is that they think coming up with good separation of concerns, good abstractions is easy. They just start implementing the first idea which comes out of their heads and then figure out the scope of responsibilities as they go.
The test for good separation of concerns is that you should be able to explain your architecture to someone with the intellect of a 9 year old child who happens to understand the business domain. I'm not exaggerating. It has to be that obvious or else you will not be able to maintain clean separation... You will not be able to maintain alignment in your team.
If the responsibilities of a specific object are somewhat vague, what will happen is that the scope of responsibilities between objects will soon blur and the messages between objects will start to look increasingly elaborate; your system will look like a bunch of horrible incompetent managers trying to micromanage junior employees using long, convoluted instructions and occasionally throwing chairs at them...
If your system is passing around object references all over the place; that's usually a sign of poor separation of concerns; passing around complex objects by reference is tight coupling, by definition. Each object, each person should be able to fulfill their responsibilities and finish the job using communication only.
Trying to achieve "high cohesion, loose coupling" in OOP has led to the creation of (supposedly) best practice recommendations like the SOLID principles, and the development of monstrosities like dependency injection frameworks.
If OOP was actually good at "high cohesion, loose coupling", you wouldn't need them.
If you think about something like a web server as an object, it has arbitrarily high cohesion and arbitrarily low coupling. You can only communicate to it through messages (HTTP); binding happens at the point in time that the message arrives (notwithstanding that this may be cached in all sorts of interesting ways in the implementation of any given server); and the web server is fully encapsulated (security flaws notwithstanding).
I think it's perfectly reasonable to argue that much of what gets called OOP doesn't deliver on the promise, but then it doesn't deliver on the premise either, and I think these are inextricably linked.
But both of them are still suboptimal because they allow objects to instantiate one another and to directly communicate with whomever they come in contact with. That creates strong coupling and you have to "fight the paradigm" to avoid it.
Other engineering domains have this figured out: components can only send and receive data through ports, they are never aware of their siblings. Only a component at a higher level of abstraction can decide how the ports of lower level components connect to one another. That completely eliminates coupling.
(You can of course replicate this type of architecture with OOP, but you can also replicate it with any turing complete paradigm, that's neither here nor there)
Smalltalk is to me the most obvious case where people somehow don't realize we had a fully live environment, where the entire IDE (or really the whole operating system) could have open heart surgery done on it while it was running, in the late 1970s!
And that's a language that is at least somewhat in the public consciousness, languages like Linda? Nobody knows them. Synchronous languages like Lustre and Esterel? Some engineers know them (and pay good money for them), but not software engineers.
By Syndicate you mean syndicated actors?
These tools are all oriented around blocks with ports. They receive data on their input ports and emit data on their output ports. They do not know where that data is coming from nor where it is going. They cannot instantiate other blocks.
To create a new block you either create a primitive one (described directly in terms of differential equations, since these are physical simulations, or as actual code, be it C or MATLAB or whatever else), or you connect blocks together to form a larger block.
This creates a strict hierarchy, connections between blocks can only happen at the same level of abstraction. You can just rip out a component and replace it with another with the same ports, or with multiple components each filling in part of the job, or you can plop another component in the middle that's only used to process the data inbound and redirect it somewhere else.
This makes coupling extremely loose by design. The paradigm enforces it.
But it is true that there will always be parts of a software system that are highly coupled. I'm not even sure if that's even a problem, unless the coupling is also highly tangled (meaning the connections are coming from too many places).
But if you want to decrease coupling between parts of a system, OOP by itself is not particularly good at it. Even something like an Entity Component System is usually less coupled than most OOP codebases because while each System is heavily coupled to the components they do work on, the Systems are usually fully independent from one another and easily replaceable.
OOP allows to create decoupled systems but it doesn't help in creating them. It is not an inherent property of the paradigm, but many people sold it as such.
"Use OOP and your application is now magically more modular! Buy my latest book to find out how!"
Since you mentioned you are a junior I guess this sort of nonsense is from before your time. No worries, that just means you won't have stupid dogma shoved down your throat. Use whatever paradigm if it helps you and avoid whatever gets in the way.
- Control the timing of when a module is activated (instantiated).
- Have multiple instances of a module with variations in functionality where those variations are not a concern to the parent module/instance.
For me, this is when OOP becomes most useful. If I can write some code once and later use it to create any number of independent instances with the same functionality which can clean up after themselves, this is generally a lot more maintainable than having one module to keep track of all the different states in an array and micromanaging (for example) the rendering and cleanup work associated with multiple distinct pieces of state.
Interfaces (a feature of OOP) can be used to decrease coupling, but the paradigm itself doesn't really provide any assistance in regards to coupling.
The default is for objects to directly instantiate other concrete objects and to be able to directly communicate with any object they at any point come in contact with. That is strong coupling, by default.
You have to do extra work to get looser coupling. Smalltalk style OO is much better (regarding coupling) because you don't have explicit interfaces, you can always replace an object with another. You can also query all live objects of a certain type and replace all of them with a proxy if you so wish. Way better.
But you can go even further in regards to loosening coupling. You can have objects communicate through a tuplespace (Linda), you can have objects receive and send messages only through ports, you can have objects send a message to their implicit parent who is then responsible for redirecting it.
Mainstream OOP is a very poor paradigm full of issues that gets mogged by everything. Composition-over-inheritance is an admission of defeat for a paradigm that lacks native delegation support (good on Kotlin for realising this).
It even gets mogged by languages like Haskell that introduced the more loosely coupled idea of typeclasses, which were inherited by Rust (traits), Swift (protocols), Go (interfaces, not to be confused with declaration-time interfaces as in Java).
We dug a suboptimal hole and stuck our head in there for 50 years.
I think OOP languages made some pragmatic decisions. Sometimes a feature which is harmful 95% of the time could be genuinely useful and safe 5% of the time... Some languages like Haskell might choose to not allow that feature at all and force the developer to find another approach which is almost as effective for that 5% of cases; that's fair enough. It's a different philosophy.
I feel like that about passing mutable objects by reference. I find it harmful most of the time but there are rare cases were it's convenient and beneficial. I've worked on open source projects were I wanted the user to be able to use the software with any database so my function accepted a database adapter as an argument.
I could have achieved a similar goal in another way but I would have had to sacrifice separation of concerns slightly. I wanted the ability to substitute any database but also wanted the component to be responsible for the persistence and recovery of its own state as this was within its responsibilities. Also this was the only violation in the entire codebase so I deemed it acceptable. It didn't pose any problems at all in practice.
I can criticize a paradigm while also using it anywhere and everywhere it benefits me ;)
Some part of the code wants to do logging. The paradigm-native solution is to just instantiate a logger object directly, but that's not really what you want to happen, because there are application-level concerns as to where those logging messages should go.
So instead of a direct new, you create a Logger interface (extra work), then use the Factory pattern (extra work) to hide the concrete logger objects that get instantiated. Ok, but now you are still creating multiple independent loggers, which will trample on each other.
So you employ the Singleton pattern (extra work), but now you can't use different loggers for different parts of the codebase.
Ok, let's instead have the application instantiate the proper loggers, and then thread them through method arguments as needed (extra work).
But now passing all those loggers around is super annoying and extremely noisy, so you create a framework that can inject them where they are needed.
Great, now you have a whole framework just to pass some logger objects around. The framework is not the problem per se, it exists for a reason, the problem is that someone felt the need for it. And it is a massive pile of complexity that is NOT worth it.
What's the alternative? Which part of this is actually caused by OOP? If you want to isolate IO, which you should, then you need to inject things that do IO and you need to specify a contract for them. And then it's nice to instantiate things or call functions without explicitly instantiating the whole tree of their dependencies.
1) Allow objects to send messages to an implicit "parent" object, which supports "doesNotUnderstand" ala Smalltalk and that by default also forwards to the implicit parent. That means you can implement "log(...)" at the right point in the abstraction without manually passing a logger around.
2) Use algebraic effects, no need to handle this with OOP. A function requests the "log" effect and a handler defined in a higher level scope takes care of implementing log.
3) Have a language with an implicit context parameter that allows putting arbitrary things in it (closest would be Odin but Odin has a fixed set of operations in the context). Because it is implicit in the calling convention methods can be oblivious to it.
4) You separate the concerns: logging is the act of recording some information. Where that information is ultimately sent or stored is a separate concern. This is the more functional view of the problem. If you follow a "model-view-update" approach like the so called "Elm Architecture", logging is just another task produced by the view function, it doesn't need any special handling.
It's not that this is a "problem" of OOP, it is that OOP doesn't give you any tools for tackling this problem. That is why dependency injection frameworks exist. Some other paradigms and non-OOP architectures don't have any trouble with it. You can also replicate those architectures in an OOP language, but that's still "contorting the paradigm" rather than it helping or guiding you towards a clean solution.
Personally I think a global singleton logger is perfectly reasonable, it is other people (the ones using DI frameworks) that think otherwise ;)
OOP has been muddied so much too, it would be interesting if we taught only the "separation of responsibilities" part and principles related to that.
Often it comes up: who has write authority, that's something we don't teach as part of OOP, yet realizing that only one thing should do writing improves the code dramatically.
Now that you see it, seems so obvious
An object holds it's own state and might change that state based on messages it receives.
Today you can find this in, for example, Elixir and Microsoft Orleans.
It's rather the other way round. There is no pre-Hewitt implemented semantics matching Kay's later 2001 claims; Smalltalk-72 was a synchronous token-stream interpreter rather than a system of independently active message-driven agents; FLEX shows processes, scheduling and quasi-parallel control, not objects, not messaging, not actor-style autonomous entities.
As someone else said in this thread, OOP is fine for modularity, the problem is people don’t know how modular a problem should be and are driven to overcomplicate the design of any application.
I have personally taken to calling OOP either Kay OOP or Java OOP to differentiate between the original more philosophical meaning, and the later language imposed features.
This sounds a lot like microservice architecture.
Except that Kay did not envision the distinct computers communicating via REST.
https://blog.metaobject.com/2019/11/what-alan-kay-got-wrong-...
Also: microservices currently require at least process and frequently even VM/Container/Machine boundaries.
In-Process REST scales that down:
https://link.springer.com/chapter/10.1007/978-1-4614-9299-3_...
Language support for In-Process REST:
The separation of objects and usage of network protocols naturally forces interaction via communication and not data sharing, so perhaps as an emergent phenomenon it converges on Kay OOP. Although no doubt Netflix Microservice OG engineers were influenced by both Kay and Java.
If I were to quibble, though: in-process implies exactly the absence of the isolation guarantees that OOP!Kay and microservices share.
(The overhead on a Boolean also makes my inner Mel Kaye burst into tears, but that's neither here nor there in this discussion I suppose.)
You're welcome!
> in-process implies exactly the absence of the isolation guarantees that OOP!Kay and microservices share.
OOP objects are in-process and are isolated using language mechanisms rather than machine/process boundaries.
In particular the "Smalltalk-72 school" which had indeed something like "message passing" (though still synchronously). Starting from Smalltalk-76, and particularly in Smalltalk-80, which is the Smalltalk we know today, the object model pretty much corresponds to Simula-67, with compiled methods dipatched via virtual method tables. The only difference is, that in Smalltalk, the dispatch goes via the internalized string address of the selector (vs. method index as e.g. in Simula-67, C++ or Java). See e.g. https://dl.acm.org/doi/10.1145/3386335.
How does Simula differ here?
Alan Kay's distaste for (static) types is just his opinion and an original contribution of IMO rather dubious value.
After the dust has settled, it seems like the most valuable parts of OOP are private data, convenience (no need to repeat the class name in a method call), good fit for some domains, and interfaces.
Not at all. matz, creator of ruby, also dislikes static types.
You may begin to realise that good designers and good thinkers dislike something because there are specific trade-offs.
> it seems like the most valuable parts of OOP are private data, convenience (no need to repeat the class name in a method call), good fit for some domains, and interfaces
This refers more to the practicality of an implementation. Ruby was always better implemented than smalltalk, for instance, even though I think most would say that smalltalk is the more "pure" language. Ruby is more practical though, IMO.
I disagree on the "good fit for some domains" by the way. A great programming language is a great fit in any domain. It just also has to designed well, which is hard, and a trade-off.
Kay also considers CLOS and related things “true OO” and those do repeat class name in method call.
Which can be easily achieved without OOP.
Scheme Structs are rather interesting, when you define a struct the language by default also exports getter and setter functions with a predictable name though you can override the default name to give them any name, privacy is simply created by choosing to not export these getter and setter functions from the module.
Privacy on the module level rather than on the class level is a far better pattern to be honest. As in other classes defined in the same module can still have access to them.
Data has value and utility on it's own --- as much as if not more than code that manipulates it. It is often not possible to foresee all the ways it can and will need to be used. So why bind anything to it?
Relational databases were invented to provide flexibility with regard to data. OOP tries to encumber it --- often referred to as an "impedance mismatch".
Failure to understand this is what leads many into microservice hell, as they try to impose a OS process wall as solution to having failed to properly design a modular architecture.
So what could perfectly be a module with direct calls, ends up being a process talking over IPC mechanisms and much higher latency, with the added fun of distributed systems.
Ironically pretty much OOP without any of the language support.
That's the natural order of things that many people work in, I doubt any "general loosely defined paradigm" can be absent from the inevitable.
Citation needed.
TFA quotes him saying almost the opposite:
> (I'm not against types, but I don't know of any type systems that aren't a complete pain, so I still like dynamic typing.)
People do not think with words: people think with things. Words serve merely as pointers to things. Some things are easy to point at; Ilyenkov talks about a cow. Some things are much harder to point at; Ilyenkov, being a Marxist, wanted to point to private property, we need to point to an object. Usually we try to talk about them using definitions, that is some sequences of words that are supposed to describe a thing. Such discussions are notoriously unproductive. The reason is that words are not really good as pointers: we have much more things than we have words and we have to reuse the same words to point to different things in different contexts. Yet in a phrase words look same so we tend to conflate these different things. As a result we get lost and go around in circles.
So instead of definitions Ilyenkov talks about a notion. Notion is something that reliably points to a thing that is hard to point at. It does not have to be an abstract thing: for example, we cannot point to radio. We can point to a household radio apparat but it has way too many parts completely unrelated to radio as a principle. So instead we build a minimal radio that has like three or four parts yet is capable of emitting or receiving radio waves.
We can do the same with abstract things too using the same approach. Let's build a minimal working model of a thing. "Working" so that the model indeed has the quality we are after; and "minimal" so that if we lose a single part, the quality is gone. Since the thing is abstract the model will also be abstract, that is it will be a sequence of words too, very much like a definition, but not quite.
(The same principle is widely used in parables: they describe some situation and thus try to guide your attention to certain qualities of it, hoping to trigger understanding. Sometimes it is hard to even name the thing they are pointing at, yet their pointing power is palpable. I myself often think about the parable about seven blind people and an elephant. You see I'm not naming the thing it points at: I don't have a good name for it.)
So let's go back to object oriented programming. What would be a minimal working thing that we can reliably call an object?
Here is what cannot be there. First, there must be no inheritance. If inheritance were required, then the first thing we build wouldn't be an object as it would have nothing to inherit from. Only the second thing would be an object and only because it inherited from the first. But this does not seem right. Second, there must be no polymorphism on the same grounds.
But at the same time if we do this:
class Aaaa
method bbbb():
...
then it is too minimal. It is hard to say how it is different from a function. It is not an object at all. Yet; there is a missing part that would turn this into a true object, but what the part is could be somewhat surprising.To answer your question:
> What would be a minimal working thing that we can reliably call an object?
You might enjoy this paper a lot: https://piumarta.com/software/id-objmodel/objmodel2.pdf
Also check out the primary author's work on COLA as well for mind-bending possibilities this would unlock.
> First, there must be no inheritance.
The paper uses inheritance, but I've been exploring the same concepts WITHOUT it, and it works as well, if not better. Composition and delegation are more flexible. My current working theory is that inheritance is overrated at best, and too dangerous in the hands of common mortals, because it makes you think on the level of idealized categories rather than concrete things.
As further reading, your explanation of things vs words reminded me of prototypes vs classes, so I'll recommend this article by Henry Lieberman: https://web.media.mit.edu/~lieber/Lieberary/OOP/Delegation/D...
Inheritance is both good and not so good. The problem it solves is in a way unique and the very fact that it solves it is indeed quite a feat. But the solution is rather crude.
In a comment nearby I put out a theory that an object is essentially a managed computation, a computation that is driven by external events. What inheritance does is that it allows us to meld two or more such computations together in a relatively seamless way.
Again, a simple but maybe not a minimal model could be that. We have a set of collections: linked list, queue, AVL tree, etc. Most of them do not have a built-in way to count the number of objects they contain. Some do; e.g. an array, but many do not. But, of course, it is not that hard to add it to any collection. Assuming we have 'Init', 'AddElem' and 'RemElem' all we have to do is to add a counter that is set to 0 at 'Init', increment on 'AddElem' and decrement on 'RemElem'. Then we could read the current value with 'NumElems'. At the same time if we do not need a counter, then we should not add it to avoid extra work. So it looks like it would be nice to somehow extract the idea of a counter into a separate computation and then add it to a collection as necessary.
Inheritance allows us to do that in a uniform and general way and this is surely a remarkable achievement. But its solution is not simple. E.g. I have no idea what is the best way to inherit here and am afraid we'll end up with a separate variation for each collection. Yet the concept of what we are after is basically that:
aaaa = new LinkedList;
bbbb = new LinkedList + Counter;
Of course it may require special definitions at the class level, but otherwise the result should be that simple, because these are all the distinctions that are important in this context. class Aaaa
(some data)
method bbbb():
(some code)
Do you think this is an object now?I believe what's missing is not just data. That'd only grant it capabilities that upgrade it to a "record" type of entity. I believe an OOP object is more than a record. It's missing behavior triggered by messages. For an object to pass or receive a message we need to have a model of a message and that requires the notion of a sender and receiver, both objects again. Seems circular, but I'm sure it could be made to work if you properly define everything. Anyway, to my mind perhaps the minimal model of an object is not at all _one object_ but a _relation_ between two or more objects showcasing the minimal "message passing" semantics.
Weird take, but inheritance could be included if you accept something can inherit from itself. A is a type of A, I mean it doesn't strike me as wrong, but it is unconventional.
Or maybe it could give it behavior if it were like that:
class Aaaa
[some data]
method handle(message, ...)
[some code]
This construction implies there are multiple messages. "Multiple" is the key difference. The part that was missing in the original sketch is a second method: class Aaaa
[some data]
method bbbb()
[some code]
method cccc()
[some code]
Now this is an object. For example, it can be a random number generator: we initialize it and then read next numbers. Or it could be a timer: we initialize it and then read the value. We do not need a fully object-oriented environment for that; there is a plenty of such things in C and other non-OOP systems.Such a thing surely has some behavior and this is exactly what we use. We are not interested in the internal data much. In fact the internal data of an object play exactly the same role as a function stack frame: it is a private slice of memory a computation keeps for itself because this is how it works. As in knitting the size of the manipulator is tiny compared to the final result and to do anything substantial we need a place to keep stuff around until we are done. And we need to be sure data stay were we've put them, hence encapsulation.
So an object is very much like a function, it is a computation that uses some memory to do its job. It is different in that in an object the computation runs step-by-step guided by external events. A function is like an object that gets the whole sequence of events at once, runs from start to finish and in the end discards the working memory so we tend to forget it exists. An object runs from message to message and keeps the working memory, which we see as "object data". This is why objects arise naturally in areas like user interface where events are truly external. But an object is actually a primary form of computation: if you can process a single event, you can use it to process a sequence; but if you can only process the whole sequence, you cannot just switch to processing single events. There are many similarities with closures, coroutines and such; I'd say they are different ways to express the same principle.
(This also means that objects are naturally mutable. Immutable objects are an aberration that arose because we've got here in a very roundabout way.)
So you say an object is like a computation stretched over time and a function that same computation but compressed into a single invocation? Like an object is a computation whose execution is suspended between messages? I can see how that ties closures, co-routines, etc together. They are all machinery to preserve execution state across time.
Generally you could say computation is traversal through a space of states and in that frame objects expose the intermediate states, the guts so to speak, and functions hide them and only expose the in-out mapping.
I feel these are two poles of some deeper principle. Ah man, I'm not well-read enough to go further than this. I kind of worry why most developers are not deeply familiar with this material because these things will inform many foundational choices we make in system architecture and we'd definitely could use some better shared vocabulary and argumentative machinery than mere opinions and "that's how we always do it".
We keep returning to it because this is the natural way to do computation using a machine, but we also try to escape because it is rather hard for a human. We like the functional form more: it looks sequential and goes nicely from start to end. With objects we quickly lose our way in a soup of small parts.
https://wiki.c2.com/?ClosuresAndObjectsAreEquivalent
So why have objects when you have closures? I believe that by externalising the decision on how to behave when a method (selector) is called, into a concept like a vtable, you can apply greater levels of optimisations.
In other words, a closure is a object that encapsulates (keeps private) the list of function pointers it is able to respond to. This is not necessary, and Piumarta (linked by me elsewhere in this thread) shows you can reach this generalisation well within the OOP system, by making vtables themselves objects which respond to a lookup method that returns a function pointer.
side note, when first transistor-based portable radios showed up (as opposed to large lamp-based stationary devices), they were called "transistor". you would take not your portable radio with you, it was your transistor.
I'm now OO pilled and feel it has really improved my Python and my golang.