Application read & write interfaces

Currently, we define applications as a pair of a set of resource logics and a set of transaction functions (for full details, see the RMv2 report). The resource logics define the transition rules of the application’s state, and the transaction functions define some specific actions which can change that state (~ writes).

We currently lack a standardized (well-defined) way for applications to define how their state can be read. I propose that:

  • We rename the current “application interface” (set of transaction functions) to the ApplicationWriteInterface.
  • We create an ApplicationReadInterface defined as part of an application definition (so an application is now a triple of (application logic, application write interface, application read interface).
  • The ApplicationReadInterface consists of a set of declarative projections of the application’s state, i.e. functions of type ApplicationState -> T for some arbitrary T.

Note that here ApplicationState can include both resources (with resource logics defined in the ApplicationLogic) and also other non-linear state written to blob storage (further details to be worked out here).

For example, a read interface for the kudos application could include functions which:

  • compute the current balance of a specific denomination owned by a specific user
  • compute the overall ownership distribution of a specific denomination (subject to available information)
  • compute the current balances for all denominations owned by a specific user and use known price data to convert them to other denominations (as UoA)
  • … etc

I propose that this read interface is entirely declarative - i.e. it says nothing about how the projection functions should be computed, but merely defines what they are. Separately, we will need to figure out how to compute different projection functions and how to efficiently route updates to interested agents. Hopefully this will map somewhat naturally to our existing publish-subscribe system.

Definitions of application composition and related operations should be able to be updated in a straightforward manner.

As a side note, this is not altogether dissimilar from a “model-view-controller” design pattern, where the resource logics are the model, the transaction functions (write interface) are the controller, and the projection functions (read interface) are the view.

cc @vveiln @degregat @jonathan @Michael @mariari for input.

2 Likes

I associate lenses to this (view + update), especially since compositionality is mentioned:

Composition of applications is of course a very cool feature, no matter how it will be implemented.

2 Likes

The definition as a triple is very straightforward to me.

As a side note, this is not altogether dissimilar from a “model-view-controller” design pattern,

While reading a bit about MVC alternatives, I stumbled upon the Model View Intent (MVI) pattern being rather new and popularized by a library called https://cycle.js.org/ in the context of android app development. It emphasizes its composability and seems to fit quite well:

Model-View-Intent (MVI) is reactive, functional, and follows the core idea in MVC. It is reactive because Intent observes the User, Model observes the Intent, View observes the Model, and the User observes the View. It is functional because each of these components is expressed as a referentially transparent function over streams. It follows the original MVC purpose because View and Intent bridge the gap between the user and the digital model, each in one direction.

Source: Cycle.js - Model-View-Intent

Other texts also mention intent interpreters (that could be LLMs in our case) and reducers (that would be the projection functions that you mention).

Image Source: The Basics of Model-View-Intent

Perhaps we can borrow from this.

More reads: Model-View-Intent (MVI) Pattern in Reactive Programming: A Comprehensive Overview - GeeksforGeeks

Question:

this will map somewhat naturally to our existing publish-subscribe system

Where can I read about the existing publish-subscribe system?

2 Likes

Nice find! Interesting to see the linguistic convergence (“intent”) as well :slight_smile:

Yes, exactly - although we typically use the word “intent” to refer to the definite user action (i.e. what they call “action”), as opposed to the indefinite pre-action, we could absolutely use LLMs as a way to help users craft their definite intents. A good topic for a future prototype!

The upcoming network architecture ART will cover this topic in detail. In the meantime, you can take a look at the (wip) specification, or ask @tg-x / @nzarin directly.

2 Likes

I think the particularly compelling aspect of logic-write-read / MVC / MVI in our case is that not only the logics, but also the actions (transaction functions) and views (projection functions) are first-class objects in the database system, so (e.g.) transaction functions and projection functions could be compiled/analyzed, and Anoma can figure out (in most cases) how to optimize routing data around to the right nodes at the right time when state changes happen.

1 Like

If the read interface only describes the output projection but doesn’t specify how to compute it, how would it work in practice? There must be some function that actually performs the computations to satisfy the read interface, how does it connect to the application? I assume these functions also have to be written by the application developers (or some compute services?)

1 Like

Yes, there must be functions that actually perform the computations. In some cases perhaps the application developer will also have to specify those, and we should make provisions for doing so. My expectation is that many projection functions will have similar structures - e.g. indexing data in certain ways - and the necessary steps to compute these kinds of views can be written once and then reused for many projection functions with different specifics but shared structure. Eventually it should be possible to automatically analyze most projection functions, figure out what known steps can be applied, and then let the developer know what (if anything) they must additionally specify.

At a high level, this problem is not altogether dissimilar from e.g. query analysis in SQL, which takes a declarative query and figures out what internal lookup structures (typically indices) to use to compute it efficiently, which tables need to be scanned/joined, etc. - although obviously we aren’t implementing SQL, and routing data around the network will require different sorts of analysis.

2 Likes

Separating the interface declaration and implementation make sense, since implementations become updateable.

It seems to me that we want to still be able to mark which implementations are compatible/tested, since composability could very well depend on implementation details.
But since there will be cases where behavior can depend on local views of the network (e.g. compute balances subject to available information), applications need to be robust against that anyways.

If I understand correctly, decomposing into intents and actions, mediated by interpreters and reducers sounds like a good approach to model this:
Here the interpreters and reducers would be responsible for providing this robustness in practice, but I don’t know how that would play out concretely.

It feels like we’re moving towards tradeoffs similar to “be conservative in what you send, be liberal in what you accept” of which there are valid criticisms. For a relatively recent perspective on that see: Maintaining Robust Protocols.

Edit: Just talked to @cwgoes and it seems we should be able to alleviate this by requiring specific proofs of correctness/consistency. Specifying the properties these proofs need to validate will be important and require careful analysis for each application.

1 Like

The point about consistency of read levels is a very important one. I think requirements will vary for different applications and network scenarios. For example, for something like multichat, it might make sense to apply updates to the state (in this case, display new messages) as soon as I hear about them over the network (even before transactions are ordered by a controller), and then update the UI again (e.g. reorder messages) after the transactions are ordered and the controller sends out a new header. On the other hand, for an application like kudos which might often want to query price data, exact consistency is very important, and we wouldn’t want to include any unconfirmed intents/transactions.

I think what we will ultimately need here is a data structure specifying different possible levels of consistency, which can be used along with the projection function(s) to determine how exactly to aggregate and collate state across the network. For example, some options could be:

  • read the state exactly as known to a particular controller at a particular point in time, only (so e.g. their state, and state on other controllers which they have endorsed, at a particular block height)
  • read the latest state associated to each controller, as known to each controller
  • read the “most recent” state, including speculative updates (intents/transactions in flight)
  • read the “most recent” state, including speculative updates judged as likely to be confirmed (here we start to introduce probabalistic models)