Asynchronous Software Components Design Note
A stand-alone Software Component is characterized by:
- a name
- a bag of synonyms
- a namespace for inputs
- a namespace for outputs
- a namespace for for children components
- a namespace for connections.
Naming of software components is relative
./
refers to the current component./i/N
refers to the Nth input (N is an integer)./o/N
refers to the Nth output (N is an integer)./c/N
refers to the Nth child (N is an integer)./x/N
refers to the Nth connection (N is an integer)./c/N/i/M
refers to the Nth child’s Mth input./c/N/o/M
refers to the Nth child’s Mth output- it is not possible to refer to a child’s children nor its connections.
Note that only Container Components contain namespaces for children and connections.
Container Component
A container component contains 1 or more children components.
Container components a composed of other components.
Container components all behave the same way
handler (e)
The input handler distributes incoming messages by distributing them using the connection namespace.
peration).
Input messages are normally distributed to children.
Input messages can also be sent directly to a container asc’s output.
initialize ()
There is no user-supplied initialization for a container asc.
From the users’s perspective, initialize () can be called, but is a noop (no operation)
A Container asc is composed of other _asc_s.
A Container asc can contain container asc_s and _link _asc_s.
It is not possible to know the implementation of contained children _asc_s.
Link Asc
A link asc contains 0 chilren and 0 connections.
A link asc has only a name
, inputs
and ouputs
.
Connection
A connection consists of
- a name (mostly for debugging)
- a sender
- a receiver.
Sender
A Sender is a reference to a child (or self) along with a tag
Receiver
A Receiver is a reference to a child (or self) along with a tag
Multiple Receivers
A Send () can distribute an message to multiple receivers.
The system guarantees that all receivers attached to the same output, receive the same message in an atomic fashion.
N.B. Atomicity is “automatic” when all _asc_s reside in the same thread.
N.B.
Bare Metal and Multi-Threaded
Note that the following applies only to implementations that employ multiple operating system threads or are on bare metal (no operating system at all).
When the system is implemented with all _asc_s residing in the same thread in an operating system, “locking” is automatic (a by-product of sequential operation) and the considerations listed below can be ignored.
To deliver an message to all receivers from one output, the Dispatcher:
- locks all receivers related to the output
- appends the message to the input queue of all receivers, N.B. the message tag is rewritten from being an output tag of the originating sender to the input tag of the receiver.
- unocks all receivers related to the output
Message
An message is characterized by two details:
- a tag
- data.
A tag is a string or a symbol that is used to qualify the intended destination of the message.
Data can be anything.
Data is usually some kind of data entity that is supported by the base language.
Message Delivery Layer
Type Checking
Layered Type Checking
Type checking in this system is not an all-in-one process.
Types are checked in layers.
Analogy: OSI 7-layer model.
Analogy: Russian Dolls.
Analogy: Layers of onion skins.
As far as the ASC system is concerned, there is only one - very simple - type: the Message.
This ASC system delivers Messages and does not concern itself with the actual contents of the messages. Further checking is the responsibility of higher-level _asc_s.
Standard Methods
Handler
Every asc handles incoming messages (see the definition of a tagged message).
Every asc has a handle (e)
method that takes one parameter - a tagged message.
Initialize
Every asc has an initialize ()
method that takes no parameters.
The initialize ()
method is called before any calls to handle (e).
The initialize ()
method is called after a asc has been instantiated.
Instantiation
_Asc_s are instantiated in a manner similar to objects in OOP.
Container _Asc_s instantiate their children recursively, during instantiation.
The instantiator returns the instantiated asc so that the instance may be used in creating containing asc_s. This is a recursive process, so that _container _asc_s can contain other container _asc_s).
Templates & Runnable Instances
A asc is defined by its template.
Templates are like classes in OOP systems or prototypes in JavaScript.
A asc can run only if its template has been instantiated into a runnable instance.
A Template can be used to produce one or more runnable instances.
[N.B. This is very similar to instance and classes in OOP systems. Templates and runnable instances might be considered to be derived from OOP. ASCs emphasize concurrency and isolation. ASC templates are not general classes like OOP classes.]
Roughly, a template defines an ASC, its inputs and outputs and its children and connections between children.
Connections are internal to the template.
Connections can only connect children ascs together (and the self container).
Connections are not exported from the template.
A runnable instance created from a template has a unique set of child instances and a unique set of connections between those children.
The shape of the connections are specified by the template, but the actual connections are between unique instances of children (and self). In other words: each instance of a template has the same kinds of connections as specified by the template, but the actual end-points of each connection are unique to the instance.
Roughly, a runnable instance creates a unique instance of an asc from a template, and, creates a unique input queue and a unique output queue for each instance.
Each runnable instance has exactly one input queue and exactly one output queue for messages.
Message tags provide finer granularity for each message.
Templates
(see discussion above)
Runnable Instances
(see discussion above)
Isolation
Messages are routed by the direct parent container of the asc, not by the asc itself.
Ascs are isolated.
Ascs cannot refer to other ascs.
Ascs can only send messages “upwards” to their containers (direct parents) for routing.
Only the container “knows” where a message originates and where it is sent to.
No Connection
Outputs of an asc can be directed, via the connection table (a routing table), to other ascs.
Connections can be directed at 0 (zero) or more other ascs.
Connections that go to 0 (zero) other ascs are termed N/C (No Connection).
A missing connection is treated as an N/C.
Messages sent to N/C are discarded.
Inputs can, also, be N/C.
An N/C input never fires (IOW: never invokes the asc with the input’s tag).
Inputs and Tags
An asc appears to have multiple inputs.
In reality, an asc has exactly one input queue.
All incoming messages are placed onto that single queue.
Each message is tagged with an identifier defining “which input” the message came in on.
Analogy: A Web server uses one computer, but has multiple ports. Ports are differentiated by intege tags called “port numbers”.
Standard Output Methods
Send
Ascs communicate by using the Send ()
method to send messages.
Messages are routed by the direct parent container of the asc, not by the asc itself.
Parameters
Ascs do not have traditional parameter lists, and receive inputs as tagged messages.
Return
Ascs do not have traditional return values, and send values using the Send ()
method.
Exceptions
Ascs do not have traditional exception handlers, but send values using the Send ()
method.
Happy Path vs. Other Valid Outcomes
One point in the design of ascs is that ascs can have more than one valid outcome.
The distinction between the Happy Path and other valid outcomes is blurred.
In some ways, ascs treat all outcomes “first class citizens”.
Exceptions are not exceptional. Exceptions are, but, one valid outcome.
Message Fan-out and Copying
Queues
There is exactly one input queue for each runnable.
There is exactly one output queue for each runnable.
Deferred Sending
When an asc sends a message, the message is not delivered immediately, but is placed on the asc’s output queue.
The output queue of an asc is emptied (distributed) only when1 the asc has finished processing one input event.
The Dispatcher distributes output messages from the output queue of an asc to the input queue(s) of other ascs.
Messages are delivered using the connection list of the container. A particular asc cannot know where its output messages are going (nor where its input messages originated).
[Edge-case: messages can, also, be delivered to the output queue of a container. This is essentially an internal Send ()
employed by the container handler routine. (All containers share the same handler ()
routine).]
Dispatcher
The Dispatcher is a distinguished routine that runs the system.
The Dispatcher invokes _asc_s and expects them to process one event to completion.
(See the section regarding long-running loops and deep concurrency).
Essentially, the Dispatcher maintains a list of every asc in the system and invokes asc - in any order - that are ready-to-run.
[Aside: Since containers cannot run if any of their children are busy (recursively), it might be possible to have the Dispatcher maintain only a tree of ascs instead of maintaining a list of all existing ascs. Each container maintains a list of its children ; maybe the Dispatcher need to keep only a list of top-level containers. This optimization is left as an open consideration and is implementation-specific.]
Asynchronous Operation, Concurrency
All ascs are asynchronous and concurrent.
[Aside: this means that ascs cannot be implemented simply as callable functions. Such functions imply synchronous operation. This is a subtle point.]
Run Forever
Ascs run forever.
There is, also, a start-up time, and, a shut-down time. Ascs run forever in the steady state time (the majority of an application’s lifetime).
Like a Event Loop
It might be useful to imagine ascs as being GUI components that run in event loops.
Long Running Loops / Deep Recursion
Ascs must not2 run for a “long time” and/or execute “deep recursion”.
Cooperative
Ascs run cooperatively.
This removes a major source of accidental complexity - full preemption.
This adds the onus on the programmer to ensure that an asc runs to completion in a “small” amount of time.
Note that, it is possible to have compilers emit loops that are not loops but self-message-sends, which would alleviate the major, non-bug, reason for long-running loops.
Currently, we use hardware (MMUs and interrupts) and Operating Systems to ensure full preemption. These techniques could be used to ensure against long-running loops.
The use of Operating Systems and the reliance on long-running loops has caused subtle accidental complexity, e.g. priorities, which lead to priority inversion.
I urge programmers to stop using Operating Systems altogether. I urge programmers to design software that is directly suited to the problems-at-hand instead of relying on one-size-fits-all technologies like Operating Systems and the current crop of 3GL, loopful[^loop] languages (e.g. Python, Javascript, Haskell, Rust, etc..
[^loop]Loops are a subset of Recursion. I urge programmers to avoid recursion. Recursion does not suit the new reality - distributed programming. Loopful languages encourage programmers to solve distributed programming problems using low-level, assembler-like operations such as thread libraries. Loops, an Recursion, should only be used to program single nodes in distributed systems. Programmers should write programs that are inherently distributed and push minor details, like implementation of nodes using looping/recursion, aside.
Resolving
Incremental “Linking”
UNIX ar.
IDE
Only the IDE can know how _asc_s are implemented.
The IDE checks that all asc_s are _resolved before executing an application constructed using components.
Composition
Not inheritance.
Execution
Example
Sample Diagram
Sample Implementation
Common Lisp
JavaScript
Python
WASM
See Also
spaghetti
References
Table of Contents
-
Actually, output queues are emptied some time after the asc has finished processing. The actual implementation of delivery is implementation-dependent. Concurrency is the rule, not the exception, and _asc_s cannot run if they are, recursively, busy (I believe that all details emerge from this set of rules). ↩