OOP: (Complex Systems)-Oriented Programming
21 Apr 2022
8 min read
OOP: (Complex Systems)-Oriented Programming
This article explores the original idea of OOP, as it has been invented by Alan Kay. OOP was invented as a "systems-oriented approach" to programming, a way to build complex systems. Unfortunately, the name "object-oriented" created confusion and deemphasized this meaning. Perhaps a better name would be something like Complex Systems-Oriented Programming.
Complex systems usually expose “liveness” property, among others – they evolve continuously, without shut downs, since the first moment they emerge, or are started. Complex systems development happens while running, it is undesirable, or even not possible to built them ahead of time and start/stop at random moments in time.
Accordingly, to address these requirements a relevant way of programming is required, to help with developing and then evolving complex software systems on the fly, never fully shutting them down. Complex systems are usually large, consisting of many units and services, interconnected dynamically and embed complex feedback loops, including non-linear ones. This is different to simple systems, which are applications which can be run standalone, do not form complex feedback loops with other applications, can be developed, started, stopped, understood statically.
Complex systems can’t be fully understood or modeled statically, the only true model is the systems itself, which evaluates, simulates own behavior, interaction by interaction. Objects in OOP were meant to be universal simulators, able to simulate whole other systems, applications, computers inside.
Interestingly, the industry did arrive at similar tools. Some interesting examples are the Internet, microservices, FRP, the actor model as in Erlang. They all are addressing the tasks and problems original OOP was designed to address, though do it with different degrees of complication, efficiency, verbosity, by not making these essential requirements a first class properties, or even primitives, to build upon.
Meanwhile, the original OOP, built with objects as universal simulators, has been adopted by the mainstream industry by a way of simulating procedural programming. This is what the mainstream OOP still is today.
A slide from one of Alan's presentations.
The properties usually considered the foundation of OOP – inheritance, polymorphism, encapsulation – exactly express ways to simulate data structures and data types with objects. Inheritance is being used as a subtyping mechanism at best, spaghetti code at worst, polymorphism is an implementation of ad hoc polymorphism on data types. The current state of the art in mainstream OOP languages is support for algebraic data types, which continues the trend of simulation of computations (data types and algorithms) with objects, vs simulations of systems.
Encapsulation is the only property aligned with the original OOP idea, but even it is mostly being used for namespacing and providing static structure to the code, rather than for dynamic abstraction over entities and their internal state.
Let’s see what properties are required to build complex, evolving, live systems. These would be:
- universal modularity → modularity provides local order, enables universal composition and evolution. "Universal" means that whole systems can be included as modules into other systems, without ever leaking their internal complexity (information entropy) into the external system. This is abstraction property (more on this later). Without modularity the system is in the state of maximum entropy, a uniform state where evolution is not possible. Examples: Computers in the Internet, microservices, FRP functions are the runtime modules in the respective systems they implement. Actors, e.g. like in Erlang, are an interesting case here, because actors are not able to simulate, contain whole other actor systems within them, they are concerned with providing the basic primitives of modularity (first order modules/first order OOP, if you will).
- late binding → to enable evolvability and modifiability we need to use a call-time dispatch of entities, including creation of new ones on demand. Late binding allows for creation of dynamic topologies of objects and relations in the system, which is an important capability. Call-time dispatch relies on interfaces, and interfaces are abstractions. Computers in the Internet and microservices connect to each other at runtime, and can wait or retry if the connection is not successful. Erlang's supervisor trees are a good example of this, and also Erlang can spawn/unspawn new processes at any time. FRP is a more subtle case here, the ability to create and dispose new FRP networks is available only in higher-order FRP systems, and special attention must by payed by the implementation to provide proper resource management and cleanup.
- runtime → evolution needs an enabling environment, which would provide means of evolution and set boundaries and constraints. In software systems this means to evaluate and instantiate new entities, collect unused ones, and enable discovery for late binding. Such an environment is also an abstraction of the physical platform and other irrelevant details the software runs on and by, it is specifically designed to implement business tasks. The Internet infrastructure and people running it are the Internet runtime. In microservices-based systems devops is the runtime. FRP systems have their runtime implemented in monads or by other means. Erlang famously has its runtime able to do hot code reload.
- messaging → late biding and modularity mean decoupling, and decoupling means communication with messages. Decoupling with messaging is also an abstraction which represents the rest of the system to an instance. Internet computers and microservices communicate in terms of network connections and application-level protocols, which is a message-based communication model. FRP systems communicate with events, streams of events, signals (events coupled with new state). Erlang actors (processes) can only communicate with messages.
- internal state → to be able to evolve and simulate things, instances in such a system must incapsulate internal state, be stateful, be encapsulated at runtime (because they only make sense and exist at runtime). Internal state is abstracted from the rest of the system, the rest of the system doesn’t see all the complexity being simulated by an instance, can only see the surface, exposed my a messaging interface.
Internet computers, microservices, FRP functions, Erlang processes all do have internal state. Interestingly, since a complex system modeled in OOP ends up being also a distributed, concurrent system, troubles with concurrency can arise. This can be avoided by making time a first class parameter of the system. Mostly, only FRP does implement this property.
See also a list of properties as given by Alan Kay on Quora.
We can see that all these properties have abstraction property. Remember, abstraction is not a vague undefined idea, but a simplification which creates a new level of meaning, new semantic objects (see Dijkstra's definition, and also Wikipedia's one). That that all the essential properties of OOP have the abstraction property is not a coincidence. Abstraction is a way to handle entropy, and managing entropy is the way to build complex systems, both from human comprehension and structural nature of such systems points of view.
Entropy is important, because software systems can be measured with information entropy, which is defined as a logarithm of number of all possible states of all the variables in the system. By encapsulating the state, and by using all the other abstracting properties, OOP makes complex systems manageable.
Now, functional programming, starting with Lambda calculus, also has abstractions as functions, internal state as bound variables, and communication by application. But it lacks mutable internal state, and function application is rigid, static binding, which limits the abilities to evolve the systems built with it. One can only design them ahead of time and link statically, but complex evolving real-life systems are hard to impossible to design in this way.
So to build complex systems in pure FP it must first implement the essential properties needed for building complex systems as described above. This means that complex FP programs also have to rely heavily on microservices of sorts, or techniques like FRP.
The analogy would be that FP is proteins of software, molecules with statically defined properties, while OOP is living cells and up. They are built with proteins and other molecules, but implement a whole new level of interaction, development, evolution properties.
OOP intersects with a number of concepts from mathematics and computer science, like the actor model, state machines formalisms, communication and concurrency models, temporal and linear logic etc.
In fact, OOP did influence or inspire some of them, e.g. the actor model and message-based concurrency models. The difference is that the actors model, and languages closely based on it, like Erlang, are concerned with modeling [concurrent] computations with the basic primitives, and do not address universal simulators and systems properties, which results in them being used to implement low-level data and algorithms-based software, vs whole systems.
Also worth mentioning that objects and messages are meant to implement algebras, and could even be used to simulate FP itself. Formal verification would be a nice property to have, but seems that it is only possible for simple systems. Complex systems can only follow strict laws and evolve, and would produce unpredictable results even being fully deterministic (as stated by the chaos theory). On the other hand, formal verification can mean many things, so it depends.
The larger and more complex the system is, the more and more it ends up approximating Complex Systems-Oriented Programming. Objects being universal simulators were meant to emulate whole computers, and the Internet is exactly this - a CSOP/OOP system which exhibits all the essential properties described above, and evolves without downtime since it has been launched almost half a century ago. On the other hand, OOP intends people to build complex applications in a similar way to the Internet - software as Internet principle.
AWS Lambda functions and alike are an interesting case. They do not fully implement all the essential properties of CSOP/OOP, but also seems like they are evolving towards them, with step functions, finops runtime. This also shows where devops is heading – to automation up to irrelevancy.
Another examples of OOP systems are microservices-based backends, microservices for frontend, enterprise event-driven architectures, actor systems like Erlang, Akka, SmallTalk and its derivatives, and so on (though, as mentioned earlier, Erlang and actor libraries in other languages are generally focused on modeling concurrent data access and algorithms on low level, do not address the systems simulation requirement).
Systems built with understanding of the essential properties of OOP and by explicitly implementing them do actually provide the proverbial 10x technological and business advantages.
I can confirm this conclusion from my own experience, from high-load backend systems to complex scalable frontends to building whole complex production-proven applications in single-digit days, vs months it would take with the industry approach.
The future of software development is probably in more [energy efficient](https://traversable.space/# essays/guts-d) for humans technologies, but the underlying machinery will have to do the systems and evolution stuff similarly to what is described above.
Eugene NaumenkoSee other articles by Eugene