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.
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.
Nice find! Interesting to see the linguistic convergence (“intent”) as well
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.
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.
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?)
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.
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.
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.
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)