Design Philosophy of AL + Bibliography

The purpose of this post is to explain and justify the design features of AL and provide resources for extra reading for interested users.

Design Principles

The AL runtime can be modelled as what we call a ‘commitment machine’, that is, a stack of transactional state machines. Each layer of the state machine provides a different function and has different memory characteristics. These layers can roughly be ordered from most ephemeral to most durable, with state transitions on more durable layers automatically becoming visible to upper, more ephemeral layers. The ephemeral layer must be the most extensible, due to the fact that there is no layer more ephemeral. PROLOG/WAM semantics are used to ensure that you get full backtracking capabilities, bidirectional execution, meaning you can perform exhaustive searches over both your database and in-memory constructs. The ephemeral layer also supports the ephemeral construction of objects through overriding allocation. Thus, unlike other object systems such as ObjVLisp (which AL draws from), new does not collapse into the allocate/initialise dual, but rather a construct/allocate/initialise trio, where construct constructs an ephemeral object, allocate possibly allocates it as a durable object, and initialise performs object initialisation logic.

Objects do not ‘exist’ anywhere in themselves, but rather are abstractions built upon relations which exist within live in-memory tables E.G., ‘the class of :a is :b’, ‘the value of slot :x on :a is 3’, which are hydrated from the on-disk command log table on startup. That is, the object tables are a materialised view of the command log. We ought theoretically be able to support, therefore, other views. We plan to make use of this in order to perform efficient bitemporal queries. The object system is therefore a metaobject protocol represented relationally.

The semantics of AL rely on message sends through inheritance chains. Methods describe admissable transformations over relations, and method clauses are equivalent to predicates in PROLOG, meaning that sends can be bidirectionally executed. This means we can send a message to an anonymous object, with the runtime figuring out which object matches the message, and the ability to ‘broadcast’ messages to all objects that accept the shape of the message comes free! The responsibility of determining how ‘complete’ each method should be (I.E., the forms in which it can be executed) is yours. Each AL run returns the Elixir representation of the state of the runtime along with the bindings discovered during the run (if the transaction succeeded). This is the mechanism by which the user can inspect their result, and backtrack if they so desire.

Each method holds multiple clauses, allowing for backtracking. When an object receives a send, it finds an applicable method object on itself, or via inheritance, and then runs the method. Note that the implementation of send does not permit backtracking over inheritance I.E., only one method object is chosen once an object receives a message. Instead, backtracking, like in PROLOG, is constrained to VM ops such as table lookups (intra-method clause selection, class selection, superclass selection, slot selection, etc). This is so that backtracking over sends is easier to reason about for the user. Multiple inheritance is also supported, as inheritance is simply described as a relation between an object and its superclass, and there can be multiple of such relations. Lookup occurs in order of definition.

All AL programs are wrapped in a database transaction. The purpose of this is to ensure that state changes either all succeed or all fail atomically, ensuring that interleaving processes do not cause interleaving state changes, and helping you to avoid stateful failures. This suggests that general best practice is to cut (same concept as PROLOG cut) whenever you perform state mutations I.E., you should perform a choicepoint commit when you do a transactional commit- otherwise, you may get unintended behaviours, such as multiple instantiation. We don’t perform this cut for you, because there are some scenarios where this may be useful to you. This should be intuitively understood as ‘if lower down in the commitment machine stack something is committed, I should make sure the reasoning I do is against that up to date state’.

The most durable layer within AL is the command log, which exists on-disk and is append-only. However, interactions between AL systems would be considered another kind of transactional state machine, even if it exists outside of AL itself. Since AL is a PROLOG, the command log is intentionally local. It represents your personal, authoritative history of commitments for a single runtime. Distributed runtimes are built through interactions with other systems, not by sharing one distributed command log. In the same way, two different observers may observe events occur in different orders, but it does not change the fact of each observer having total epistemological authority over what they observed. However, we aim to provide plenty of support through libraries to ensure working as a part of a distributed system is as natural as possible. Concurrency is supported via asynchronous message sends. Polling, task dependencies, and more advanced features will be supported in the future.

As the user works on their object system, they will develop new protocols on top of the default send, and make great use of it. In fact they probably won’t want to keep typing out my_send every time they want to run. Thus, dispatch will have to become configurable such that users can define their own lexical contexts which override the default dispatch logic. Therefore AL needs a macro system.

Bibliography

A list of resources that can help the user to understand the hermeneutic that led to the design of AL

Major

Warren’s Abstract Machine - A Tutorial Reconstruction

A Simple Reflective Object Kernel

Update Reconsidered (Here’s a summary by the wonderful Jeremy Taylor of XTDB/JUXT, the paper itself is hard to get a hold of!)

Database, Types, and the Relational Model (The Third Manifesto)

Urbit: A Solid-State Interpreter

Agent-Oriented Programming: From PROLOG to Guarded Definite Clauses

The BEAM Book

On Understanding Data Abstraction, Revisited

Art of the Metaobject Protocol

The Common Lisp Condition System

Object Oriented Programming in Concurrent PROLOG

Minor

Semantic Spacetimes

Pluggable Type Systems
https://bracha.org/pluggableTypesPosition.pdf

Out of the Tar Pit

Sea of Nodes

3 Likes

I wonder, our construct is similar to allocate in ephemeral languages would allocate/store/initialize be more apt? Even this language is improper for explanation, allocate/persist|project/initialize may work. In essence allocation is in a lot of ways a projection of the db allocation so maybe project/allocate/initialize. It’s hard to come with a sensible name for the precise meaning we mean.

Why does this follows? Could I not argue persistence is of most extensible importance as there are many ways in which I may care about peristance?

This is a very elegant way of describing multiple inheritance you can explain different search methods nicely by describing what custom unification order you may wish to make the inheritance search to work. It would pair nicely as for CL the mechanism is rather complicated meaning a more complicated unification search is required.

1 Like

I think that allocation in smalltalk etc means specifically the storing of object in memory as base behaviour. Thus if we are basing our system off of smalltalk then I think it would be best to look at either having two kinds of allocation or splitting into a trio. However I think these two behaviours are sufficiently different to require different names. The reason I chose the name construct is because it feels analogous to the process of creating a mathematical construction.

You bring up a good point regarding persistence being extensible, but this is a different kind of extension as the ephemeral layer. Persistence can be extended by adding new layers, as can ephemeral. But you would like your ephemeral layer to not need to be extended through addition of alternate layers as much as possible. Perhaps my argumentation here is lacking. But yeah you can imagine adding a new durable layer and overloading allocation sends it to that layer. Making different persistence layers that ‘exist on the same level’ interact cleanly sounds non-trivial though.

Indeed different inheritance mechanisms can be conceived of, and I believe it would be worthwhile to spend more time discussing what kinds of inheritance search algorithms might be used in order to give the user more power over their system. For example, CL makes use of an inheritance matrix to determine the order. We might imagine the user re-arranging the order of the superclass relations in order to change this inheritance matrix. At the same time, this will still be DFS. We have spoken before about using meta-interpretation to overload the send algorithm, and I believe this could be one method. However for more efficient searches, this would likely require an alteration in the runtime?