Object systems for applications: step-by-step

Preamble

Recently, I’ve been investigating what it might mean to model applications as object systems. This involves many aspects, some of which are “essential” (in the sense that they must be considered holistically), and some of which are “inessential” (in the sense that they would be little more than syntactic sugar compiled to a simpler representation) – and it’s not always clear which is which. This post is an aim to construct some critical components – basic definitions, transactionality, determinism/non-determinism, information flow control, and compilation to resources – in a step-by-step way which carefully teases apart what they mean individually and thereby allows us to see clearly how they might relate to each other.

This post will define important terms in bold.

Basic definitions

What are applications?

The SGS ART report will discuss applications from first principles, but here I will just adopt a working definition: an application is a subset of Anoma network state and associated functionality which provides a user-facing interface surface oriented towards a particular purpose, such as credit tracking (kudos), social organization (multichat), or deciding where to go for lunch (LunchBot). An application is singular (unified) in virtue of this purpose: if it’s unrelated to lunch decision-making, it isn’t part of the LunchBot application.

What are objects?

For our purposes, an object is anything which you can send messages to and get responses from. This is a purely behavioral definition – an object is as an object does – and it is both necessary and sufficient: if you can’t send messages to (specifically) it, or can’t get responses from (specifically) it (eventually), it isn’t an object.

From this basic definition we can derive some corollaries:

  • An object is fundamentally relational: an object is an object to whomever can send messages to it and receive responses from it (perhaps, but not necessarily, everyone). It’s entirely possible that something may be an object to me but not to thee.
  • In order to send messages to one object as opposed to another, I must be able to distinguish between them. Therefore, an object must have some sort of name from the perspective of whoever is able to send messages to it. For the remainder of this post, we will use numbers to indicate this name – e.g. O_1, O_2, etc. – but this is just convention, names need not be anything in particular.
  • From the perspective of whoever is sending messages to it and receiving responses, an object has a history: the sequence of messages sent and received. We will also call that sequence of messages a trace.

How can we characterize objects?

So far, we haven’t talked about how objects behave – we’ve just said that you can send messages to them and get responses. However, in modeling applications as objects, we will be constructing objects, and when we construct objects, we can specify how they (ought to) behave. Let’s dive into what that might look like.

A basic first step in characterizing the behavior of an object – which we will call the object type – is to specify what kinds of messages you can send to it and what kinds of responses you might get back. This entails two types. Let’s call the first type the input and the second type the output. In the simplest sense, then, an object type is simply a pair of two types (the input type and the output type).

type SimpleObjectType I O = (I, O)

We will use the word “type” somewhat loosely – it can be understood as a synonym for “set”.

This simple object type tells us what kinds of messages we can send to the object and what kinds of responses we might get back, but it doesn’t say anything about when we would get certain responses. How might we characterize that? Remember that objects have a history – outputs aren’t necessarily a pure function of inputs. The most basic thing we could know about what kinds of responses we might get back is then a property of all potential histories, which we will call a trace invariant:

type TraceInvariant I O = [(I, O)] -> Boolean

Histories where the trace invariant holds are histories we might observe. Histories where it does not we will definitely not observe. What history we actually observe, of course, depends on which messages are actually sent to the object in question.

With a trace invariant, then, an object type consists of an input type, an output type, and the trace invariant:

type ObjectType I O = (I, O, [(I, O)] -> Boolean)

Note: we are assuming here that messages are totally ordered. We may revisit this assumption later but will take it as granted for now.

Now, this trace invariant is just an invariant: it specifies what histories may be observed, but does not necessarily allow us to compute what the response of an object would be given a particular history of inputs (since there might be many possible responses). If we want to specify the ability to compute that, we are specifying a deterministic object with a characteristic response function, as:

type DeterministicObjectType I O = (I, O, [(I, O)] -> I -> O)

Note that from a characteristic response function we can compute a trace invariant: the trace invariant is true for the output determined by the characteristic response function, and false otherwise.

One possible way to implement a deterministic object is for the object to keep a state which keeps (and typically compresses) whatever information about the history of interaction is needed in order to compute the response to the next input. From the perspective of the external interface, though, this is just one possible implementation – an external observer doesn’t know anything about the “state” of an object or lack thereof, only its observable behavior.

For the remainder of this exploration, we will work mostly with the standard (not necessarily deterministic) object type.

What would it mean to model an application as an object? Let’s take a very simple version of kudos, specified as follows:

  1. A party A (parties are identified by public keys) should start with 100 kudos.
  2. Any party should be able to transfer kudos to another party (authorized with a signature) and read their balance.
  3. After a transfer of amount X from source S to destination D, the balance of S should decrease by X and the balance of D should increase by X. If S’s balance would become negative, the transfer fails (and has no effect).

Let’s model this simple kudos application as an object. To start, the input and output messages:

Now, what should the trace invariant be? In this case, we can describe it recursively:

calculateBalance [] a = if a == A then 100 else 0
calculateBalance (x : xs) a = calculateBalance xs a + case x of
  (Transfer source dest amount, OK) = if
    | a == source -> -amount
    | a == dest -> amount
    | 0
  _ -> 0

traceInvariant xs : [ (getBalance a, balance amount) ] = amount == calculateBalance xs a
traceInvariant xs : [ (transfer s d a sig, result) ] =
  result == validSig sig (s, d, a) and a >= calculateBalance xs s
traceInvariant [] = True

If an object behaves like this, it is a valid implementation of this kudos application as we have defined it. Now, how might we implement an object which behaves like this? We could, of course, create a specific object with a state (balances of different users) and a transition function, but this would mean that we have to place that object somewhere in our distributed system, and notably that reads and writes to that particular object would be more ordered than they need to be: independent transfers do not need to be ordered with respect to each other in order to satisfy this invariant. Instead, therefore, we will attempt something else: to decompose this object (type) into a system of smaller object (types) which, taken together, provide the same external behavior.

We will call this problem the problem of object decomposition: to take a description of a higher-level object type and craft a system of lower-level objects and messages between them that, taken as a system, behave as if they were a higher-level object without a need for an actual, named higher-level object anywhere in particular.

Transactional object systems

Let’s now investigate object systems: collections of objects interacting with each other in a shared context. Remember: we want these object systems to behave as if they were one higher-level object. That higher-level object either receives a message and responds or does nothing at all – messages are atomic, there is no “partial receipt”. This means that our object system must be transactional: even though multiple messages may be involved internally, each transition initiated from the outside must either fully complete or have no effect at all. Let’s draw this:

The basic execution flow would always be as follows:

  1. An incoming message comes into the system (as would be sent to the higher-level object).
  2. Internal message-passing happens between objects in the system.
  3. A response goes back out (as would be sent by the higher-level object).

How should these external messages trigger internal messages? A start is simply to model this relationship as the implementation of the higher-level object, which has no state and must handle incoming messages by making calls to the lower-level objects. These calls must all happen within a transaction, which either completes successfully or has no effect. As a diagram:

What would this object system look like for our kudos app example from earlier? A simple option is to keep a “balance” object for each public key, which we subtract from or add to on a transfer and read on a balance query:

Note here that implicit in this description is a balance object associated to each public key, which starts with a balance of 0 – and the high-level “SimpleKudos” object must be able to look up these balance objects somehow.

Let’s write this out as an internal transaction invariant scoped over the message sent to and response sent from the higher-level object and the messages & responses to and from the lower-level objects, where nameOf captures this nominal relation described above:

transactionInvariant (Transfer s d a sig, OK)
  [(s_o, Subtract a, OK), (d_o, Add a, OK)] =
  nameOf s_o == s &&
  nameOf d_o == d &&
  verifySig sig (s, d, a)
transactionInvariant (Transfer s d a _, Failure)
  [(s_o, Subtract a, Failure)] =
  nameOf s_o == s
transactionInvariant (getBalance a, balance a)
  [(a_o, Read, ReadResult a')] =
  nameOf a_o == a **
  a == a'
transactionInvariant _ = False

So far, so straightforward. Now for the sleight of hand: nowhere in this transacation invariant is the higher-level object actually referenced. Thus, we can erase it: as long as this invariant is checked in any transaction involving the lower-level objects, the observable behavior of the system will be identical to the desired behavior of the higher-level object (as we intend).

This invariant, however, is still singular, and we must decide how to trigger it. Let’s proceed as follows: for each lower-level object, encode a check for the subset of the invariant which mentions that object, which gets triggered whenever that object is modified.

General process of compilation

  1. Take a system described by a higher-level object and express it in terms of a transactional system of lower-level objects, described by a transaction invariant (as in the above example).
  2. Partially evaluate the transaction invariant on each lower-level object in order to obtain the invariant which should be enforced by that specific object.
  3. Transaction environment must include (a) external inputs + outputs [messages to/from the higher-level object] and (b) messages between the objects at this level.

This can be repeated recursively by carrying constraints through the recursive passes, which will typically reference intermediate messages that can then be simplified out (into more complex constraints). I haven’t worked out all the exact details yet, there’s a stab here.

To illuminate roughly how this can work, let’s run through the full example for kudos, where we implement the balance objects in terms of immutable objects which simply store a certain amount assigned to a particular public key, and can only be created or destroyed. In a diagram, the application now looks like:

Let’s examine the balance object transaction invariant:

transactionInvariant (balanceFor a) (Subtract amt, OK) objs
  = all (ownedBy a objs) and
    sum (consumed - created objs) = amt
transactionInvariant (balanceFor a) (Add amt, OK) [(NewImmutableObject a' amt')]
  = a == a' and amt == amt'
transactionInvariant (balanceFor a) (Read, ReadResult amt) objs
  = sum (map amount objs) == amt
transactionInvariant _ _ = false

I’m eliding some detail here concerning how we ensure that all relevant immutable objects are looked up in the balance query, but let’s leave that for a future, more refined version – it’s inessential to the essence of the proposal here.

We can now erase these intermediary balance objects by combining the transaction invariants as described above, and voila: a kudos application compiled down from a higher-level object representation to constraints on immutable objects!

This process needs to be worked out in full detail, but I wanted to share this direction for feedback first. Note a few particularities of this flavor of object system:

  1. Objects do not have state, except for the final level of immutable objects. Higher-level objects can only create, destroy, and send messages to lower-level objects.
  2. A hierarchy is implicit: we can think of the lower-level objects, in some sense, as within the higher-level objects. Lower-level objects within a higher-level object cannot be referenced elsewhere (so this is also a nominal scope).

Brief notes for self on future topics to explore in this area:

Intents in object systems

Let’s extend the kudos example with a “swap” input. This will require two changes:

  1. We’ll need to extend the application to be able to refer to multiple denominations (A-kudos, B-kudos, etc.).
  2. We’ll need to add an input “Swap(denomA, amountA, denomB, amountB)” which allows the user to swap amountA of denomA kudos for amountB of denomB kudos (if a counterparty can be found).

Let’s first extend the existing application with different denominations, which is straightforward:

Now, let’s add “Swap”. At a high level, the inputs and outputs should look like this:

where either the swap completes successfully, and the swapping user’s balance increases by amountB of denomB and decreases by amountA of denomA, or the swap does not complete, and no balance changes happen.

Now, how might we implement this? The user in the swap example wants to find a counterparty, so let’s invent a magic “counterparty discovery” object, which will either find them a counterparty (who wants to swap in the opposite direction) or fail (if no counterparty is found). If a counterparty is found, simply execute the transfers – if not, do nothing. In a diagram:

This is the basic pattern: represent counterparty discovery as an object, which either returns a suitable counterparty or fails, and perform subsequent state changes accordingly.

TODO: Figure out the compilation here, it should result in two opposite swaps being able to be matched because “counterparty” is a free variable. Also figure out the difference between one counterparty and multiple counterparties.

Information flow control

The basic thing we want to do here is add annotations to specific messages (methods) which specify who is allowed to send (call) them. As a first pass, we can limit the scope of these annotations to data in the method itself (so annotations cannot depend on any state). This is sufficient, for example, to specify that only the owner should be able to read their kudos balance:

Now, this metadata, described as a constraint, must be passed through compilation and eventually operationalized (e.g. by verifiable encryption). In order to do this, we need to track the decomposition of messages (e.g. whatever getBalance is implemented in terms of) and pass through the constraint (only A should be able to send those methods), where the constraint can be operationally realized at the final level by mechanisms such as verifiable encryption (which must be tracked holistically, since e.g. someone transferring kudos must now encrypt the outputs). Some combinations will not be possible to compile, e.g. an input that is only allowed to be sent by A and another only allowed to be sent by B, where both translate to reads of the same object (a smart compiler could show an indicative error message in this case, and the user could perhaps change the annotation to make it work).

This should already be sufficient to make a lot of basic things work. More complex iterations could allow for history-dependent annotations (e.g. as in a messaging app - should be able to read messages sent to me), and this would ultimately need to be incorporated into the trace invariant (which would then be additionally scoped to “who is calling” beyond just the inputs and outputs).

  • Similar to Viaduct, we should be able to search for cryptographic primitives to implement the desired IFC properties (but modeled in this object-system-way).
  • Analyze many more examples.
4 Likes

I would add

… without the intervention of an external user.

I think, there are suitable ways in which applications may interact, for purposes that are not captured by either one of a set of applications, but only make sense for all applications together.

I think, it can be made explicit in a (fully expanded) message sequence chart with scopes as described here.

In the examples, outputs are often several messages to several objects (or they should, ɪᴍʜᴏ). Thus, in several places, O should rather be a finite set of O.

Writing {O} for the type of finite sets of O, I would rather expect

type DeterministicObjectType I O = (I, O, [(I, {O} )] -> I -> {O})

Or is there a fine point I am missing for why we need to also send one message at a time?

What in your definition isn’t an object? Does this definition present benefits over objects just knowing themselves? Everything you’ve described can work with either definitions.

What is the benefit in categorizing things in this way?

Do all objects have a trace? I notice you mention immutable objects near the end, do they also have a trace? Does 1 have a trace? Is this down to peculiarities of how we want our objects to have names and how we view evolution.

Something interesting is how the principled book about Databases mentions values vs variables, and general differences there in.

Also In your example later on with simple-kudos-object, how does it’s history update, if the issue of place comes up and it’s immaterial, then how would a trace work in these instances?

Let us not say the object has no state, you later on mention

A better way to describe this without reinventing terminology, is that the higher level object has a reference to these objects somehow. Either through having it directly in it’s own slot, or in the message it gets having a reference to these objects (your example of balance changing shows it in a message, and a previous message and this explanation shows a different strategy).

I believe newspeak talks a lot about this, as even invoking GlobalClass foo requires you having a reference to class to begin with which is something that also must be passed in:

should be for great stealing.

Generally the issue I have is that the state is in the current state of the system and knowing where to get these data from, they do not appear out of the void I don’t believe.

After some discuss I think how state is used can be refined as I don’t think it’s communicated too clearly in this post.

I believe having an entry point object is a generally decent idea, as one always needs an entry point.

These could be graphical objects or just ones you type, for example in the forum, this post can be an object and reply the message we send to compose our messages, on linux this is the ls in my terminal and the -thor i pass to it. Just like most things these are nice objects to think about a more complicated system hidden in an unified interface with it’s environment.

2 Likes

I think my definition does provide benefits over defining an object as something that knows itself, yes – from the perspective of an external observer, where an object “knows itself” or not, and in what way, is an irrelevant implementation detail; the external observer can observe (and thereby care about) only how the object behaves.

All objects understood in the way I define them have traces, yes. Whether or not the symbol 1 represents an object (and which object it represents in which context) is a separate question.

I don’t follow this question :confused: cool video though!

There are indeed some references / a namespace / a context involved here, yes, which should be more precisely characterized. “Slots” from the perspective of this definition (and, indeed, whether a particular object has state or not and of which sort) would be an implementation detail and thereby not fixed by an object type. I’m trying to be careful not to import any more concepts than strictly necessary (where “slots”, what it means for an object to “have a reference”, etc. are all concepts that add “complexity baggage” to our model, which I want to minimize as much as possible while retaining the capabilities that we want).

I’m asking how do the traces work for these immaterial objects. My previous understanding was that this was a trace for all time rather than it being scoped to a single transaction. If I’m incorrect I do wonder how these transaction traces get updated as my previous understanding was that it seems like interacting with an object appends to the trace of the object over time to give a view on how something has evolved, which brings the question on how this works generally.

I’ll have to think about this, I need to look at some literature if this is a previously used definition for objects (Joe Armstrong talks about Erlang being OO done right decades after [he really disliked OO as it was stated], however I believe the way in which it was meant was more in lined with cook. But if my interpretation is wrong, then that could be good evidence for this definition).

2 Likes

Yes, trace invariants are scoped to the object’s history over time (but the messages in a particular transaction either happen atomically, i.e. all of them happen or none of them do).

I think my definitions are pretty in line with the Cook paper, for example:

The interface type here is exactly that of my “SimpleObjectType” above, specifying just input and output types…

… and in my formulation:

  1. The trace invariant is a “behavioral specification” as described by Cook, which can be mechanically verified (either the observed history confirms to the trace invariant or it does not).
  2. Internal objects cannot leak because of isolated namespaces (this is implicit in the above model + does need to be more clearly described).
  3. Method re-entrancy can be similarly prohibited by hierarchical namespaces (where lower-level objects called during method execution simply don’t have a reference to the higher-level object).

In other words, what I have described is really a “layered object system” with a namespace and call hierarchy, which seems very similar to the “layered ADT system” Cook describes in the last paragraph, but with objects!

The high-level object system and programming transaction and projection functions in terms of invariants over objects seems nice and would definitely make programming applications and probably composition of applications easier.

Two questions to get a better understanding:

  1. Can immutable objects be shared between multiple high-level objects, or are they uniquely associated with one higher-level object? If yes, could there be conflicts, e.g., higher-level objects wanting to create/consume the same immutable objects as part of a transaction?

  2. How do permissions work in the higher-level object system? Does the SimpleKudos Object have permissions on the B balance objects?

1 Like

That depends on how the application is constructed, but in principle, this is possible, and it could indeed create conflicts (but this would be a poorly designed application - an application designer should be able to write their application in a way which avoids this).

In this formulation, implicitly, yes (no one other than the objects sending the messages described in these diagrams “has permissions” in the OO sense).

Are you expecting to be able to make historical queries about your system? If so, do you plan on being able to reconstruct objects from their traces, or maintain a timeline of what your objects were at a given point?

Normally for declarative systems that involve querying between entities, you would usually opt for time as a first class argument, see Daedalus https://dsf.berkeley.edu/papers/datalog2011-dedalus.pdf

2 Likes

Great question. I don’t think we have a fully certain answer yet. We certainly want to be able to make historical queries, although there may be some limitations on queries executing within the system environment (where any particular transaction is executing), since that environment will not have synchronous access to most of the (distributed) system.

If we take this object system and compile to / implement it with immutable resources, those resources (assuming that someone stores them) would provide the requisite historical record to reconstruct the “object system history” (where each object has a history/trace of a sequence of messages, and the index in that sequence could be understood as a logical time for that object). That would also allow future state transitions to depend on the history of particular objects if they so desire. Thanks for sharing the paper – I must admit that I only had time to skim it, but this looks very relevant – and I think we should indeed be able to model time as a first-class argument in e.g. scry queries into past history.

I would like to develop a fully self-contained “object-system-historical-model” which includes such queries and then describe how to implement that on the substrate of immutable resources (which is what we actually use, but the levels should be separate). I think you’re quite right that time should be a first-class argument here, I will think about this some more (and ideas welcome).

1 Like

I would like to develop a fully self-contained “object-system-historical-model” which includes such queries and then describe how to implement that on the substrate of immutable resources

Given what you’re describing, the work in the 90’s on bitemporality in SQL (Dedalus had its roots in this also) will likely be of eventual relevance to you. I would guess that in a distributed context an anoma node would need to be updating its own knowledge of the historical facts, and thus would want to have access to (and possibly update) not only information like the times that a given resource or method applies to, but also when it learned that fact. In the context of an object system, I would be inclined to think this looks like objects being parametrised according to those two timelines :slight_smile:

2 Likes

As we are in a distributed system, it has occurred to me (just this morning)that the history of messages could be naturally taken to be a (linearizable) partial order instead of a total order. For the specific case of the Kudos application, if two transfers A→B and C→D may be independent of each other, and thus, the order in which they are executed is immaterial.

1 Like

Writing down an improvement based on a discussion with @mariari:

Passing objects in messages

We want objects (of a particular type) to also be able to be used as part of method input data. For example, a properly generalized version of kudos would not take public keys and signatures but instead some kind of “Account” object with an “authorize” method, where the implementation thereof can be opaque – thus the definition of kudos captures only the minimal transfer and query logic, and how exactly transfers should be authorized can be defined by one’s account.

Now, how can this be implemented? Two cases:

  • Stateless account object logic (e.g. as would be implemented with a public key) – here we can effectively inline the stateless check (which would then be equivalent to how public key signatures were checked before).
  • Stateful account object logic – here the object must be passed by reference (which may need to be a dynamic reference, e.g. involving post-ordering lookup), and the authorization call is checked by looking up this object and calling authorize.

For simplicity, it probably makes most sense to pass objects by reference in all cases, and have some intelligent internal logic to distinguish between stateless references / fixed-resource references / dynamic-lookup references.

Passing applications as objects

Now, can we reuse this same technique for applications themselves? I don’t see why not – but we need to pass information in a way such that the “virtual application objects” can be compiled out as they are in the system described above. This is effectively a more complicated sort of stateless reference, where calls to application objects should be compiled out to (transactional) sets of calls to the sub-objects (and recursively thereof until resource logics or similar atomic state predicates). Then, if we want to, we can implement kudos swapping not by adding functionality to the kudos app but rather by writing a “KudosSwap” app:

The end result has identical behavior – but this way we keep Kudos small and simply build on top of it. Kudos itself need not know anything about swaps!