This image represents the future of programming. In fact, it's also the present of programming.
CPUs are cheap and plentiful.
Standing on the Shoulders of Giants?
I think that it does not make sense to stand on the shoulders of giants who were solving a completely different problem, i.e. how to make do with only one CPU.
Most of our current programming languages are based on the 1950s notion of "there is only one CPU, so, time slice it and optimize for speed"1.
Due to this 1950s bias, we only have caveman-level technology for programming systems composed of many distributed nodes (and CPUs). At best, our languages let us program the innards of single nodes, then use assembler-like "primitives" to join nodes together, e.g. thread libraries. We need languages that rise above that bare minimum and allow us to easily choreograph networks of nodes.
When we target computer hardware that uses CPUs, there is but one programming language - assembler. All of the other HLL stuff (C, Haskell, etc.) are just tools that help developers make fewer mistakes when creating assembler.
As a first step towards the future, development systems should simulate the concepts of many isolated nodes, simulating asynchronous message passing between them, and, IDEs should provide tools for developing and debugging such systems.
Instead, current development languages simulate multiple processes that use shared memory (in the name of “efficiency”) by default. Shared memory, by default at a low level, is not realistic in our current and future realities.
Using the old-fashioned model of time-sharing coupled with shared memory has caused an avalanche of gotchas, workarounds, and, drain of brain power. One of the most glaring examples of this kind of failure was the Mars Pathfinder fiasco[1].
Aside: multi-core technology is just more-of-the-same, i.e. multiple CPUs sharing the same blob of memory and hardware.
Modelling Future Reality
A model for the future reality is: single-threading and complete isolation.
Private, not shared, memory.
If you need to use another thread, just grab and allocate another Arduino (rPi, or whatever).
LEGO® blocks of software that snap together via one-way, asynchronous message passing.
Do we even need operating systems?
We don’t need processes, we have closures and we know how to create queues.
We don’t need function libraries and drivers if we have LEGO® block software components. We can snap-in only what we need for each application on a per-application basis. We don’t need 55,000,000 lines of code to give us everything, including the stuff we don’t need when developing specific apps.
Asynchronous Message Passing
Creating asynchronous message passing is straightforward:
Create 2 kinds of components
Recursively defined Container components, and,
At the bottom, flat Leaf components
Software components are like high-level Lisp lists[2]. Flexible, layered, recursive.
Leaf components are the “Atoms” of component based design. Leaf components contain code.
Container components contain children components and connections between them.
2) Give each component exactly 1 input queue and exactly 1 output queue, regardless of the number of input/output ports.
Each incoming message is tagged with the port ID that it came in on. Messages are queued in order of arrival.
Outgoing messages are queued, in order, tagged with their target output port names. Output messages go only to the output queue. Components’ parent Containers route messages to their destinations. Containers do the routing, not the senders. Senders cannot hard-code destinations into their own code. This increases flexibility and LEGO®-ness. A Container can wire up its children however it wants. Components can appear in many different Containers, wired up in different ways. [Aside: a single component may appear more than once in any Container, too].
The problem of deadlock doesn’t go away, but, is pushed up to where Software Architects can deal with the problem explicitly, instead of having the problem hard-wired into the bowels of the programming tool. A queue-per-port strategy invites deadlock. A single-queue strategy in the tools avoids implicit deadlock in the tools.
3) Augment our notion of programming languages by adding syntax for "send" and for "message handler”.
Something like `f⟨x⟩` - meaning "send x to output port f” (queue the message on the output queue, tagged with ID f) - in addition to `f(x)` - meaning "call function f with argument x”. Components may not CALL functions in other components. The only means of inter-component communication is asynchronous, one-way message-sending.
Message handling looks like any normal function that takes 2 arguments
Self
The incoming Message
4) Containers implement 4 routing strategies
Down - A Container punts an incoming message down to one of its direct children components
Across - Output messages from one child are routed to one other child component.
Up - Output messages from one child are routed to an output port of the Container.
Through - A Container moves an incoming message directly to one of its own output ports.
During routing, messages’ ports are remapped to destination ports.
A connection is a triple2 {direction, sender/port, receiver/port}3.
Multiple connections may be fired at one time, i.e. an output from a child component might be routed to many other children components,
or, an incoming message might be routed down to more than one child component,
etc.
…
Hence, routing must be performed in an atomic manner to avoid possible interleaving of messages from other places.
5) Containers must process each message to completion before inhaling another message.
In other words, a Container must wait until all of its children have reached quiescence before inhaling the next message in its input queue (if any).
This strategy has been successfully implemented and prototyped in 0D[3]. The concept of Sending vs. Calling is further discussed in Call/Return Spaghetti[4].
Living With The Past
I’m not advocating that we delete what we already have, i.e. advances and code written in a function-based, synchronous, textual manner can, certainly, be used.
We can build tools for the future paradigm using systems and languages that we already have. That’s what they did in 1950. They used what they had - electronics - and built upon it. We have a more modern layer of software and more modern techniques that we can build on top of. Further tweaking of existing techniques and programming languages will inch us closer to the asymptote, but, to jump over it, we need to do something radically different.
Side Benefits
Once you achieve asynchronous message passing, you can draw sensible diagrams of programs.
It is difficult, if not impossible, to draw sensible diagrams of synchronous code. Box-and-arrow diagrams and ideas like Actors have gotten a bad reputation because they have been implemented in the synchronous paradigm, using synchronous (textual) programming languages.
So-called VPLs - Visual Programming Languages - like Scratch[5], are but visualizations of synchronous, textual code, instead of being true VPLs.
Bibliography
[1] Mars pathfinder disaster from https://guitarvydas.github.io/2023/10/25/Mars-Pathfinder-Disaster.html
[2] Layering Code from https://programmingsimplicity.substack.com/p/layering-code
[3] 0D from https://github.com/guitarvydas/0D
[4] Call / Return Spaghetti.
[5] Scratch from https://scratch.mit.edu
See Also
Leanpub [WIP]
Twitter: @paul_tarvydas
By 1950, I mean “early computing”, 1950/1960/1970/1980, etc. 1950 happens to sound better.
Not a double.
Where “direction” is one of the afore-mentioned routing strategies - down, across, up, through.