Skip to Content

So after we looked into the motivation and key ideas, and dabbled in code a bit, we can take a slightly deeper dive into the language – how different constructs interact to help us describe an application (minus UI), easily and coherently.

In this post, I plan to elaborate a bit more on the underlying model of the language, and show how it manifests in different constructs. This is not intended to be a language tutorial or an exhaustive guide to syntax/semantics. For this purpose, I suggest you visit the developer’s guide. Also, keep in mind that the description here isn’t necessarily very rigorous, for the sake of readability and brevity, so forgive the occasional “hand waving”.

Definition – It’s All Objects

The fundamental building block of a River application is the object. An object is essentially anything that I would want to express something about in an application. It is versioned and (usually) named and therefore the basic unit on which we define the lifecycle of the different application components.

An object has a set of members – actions and elements. An action is a piece of business logic, in the form of several statements. An element is a named and typed piece of data. So the basic picture is:

/wp-content/uploads/2013/12/basic_object_353190.jpg

(Side note: you can view a data element as a combination of two actions – an accessor and mutator. This isn’t crucial at this point, but might prove useful later when we discuss access control, overriding, encapsulation, inheritance and redefinition of objects. For the sake of simplicity we treat it as a distinct type of object member for now).

There are different types of objects in River. Each serves its own purpose, defined somewhat differently and may obey slightly different rules, but they are all objects. This view allows us to define lifecycle processes (e.g. assembly of an application) uniformly on all objects, regardless of their type. It also serves the purpose of openness: it allows a River application specification to consistently refer to objects that are defined in completely different stack/technology, regardless of how the object is defined. For example, a table defined using SQL can be referenced as an object in River (and queried, updated, etc.). One could also refer to a completely external service defined in whatever technology and exposed using a RESTful OData endpoint. This allows us to combine different stacks coherently and modularize an application properly. It is the job of the River compiler to resolve the correct implementation and generate the necessary code to access the referenced object.

The two fundamental types of objects are a namespace and a data type. A namespace is an object containing other objects, thereby qualifying them with a name. A data type is an object that carries the definition of data elements inside it, usually to be manipulated in the application. The fundamental kinds of data types are structured and scalar (there is also a collection type). Out of the structured types, we distinguish between persisted objects and non-persistent objects. Persisted objects are objects that are associated with some data store, essentially defining the basic data manipulation actions.

The fundamental taxonomy of objects is therefore:

Fundamental Taxonomy.jpg

For example, one could define a structured type called Address like this:

type Address

{

     element houseNumber : Integer;

     element street : String;

     element city : String;

     element region : String;

     element country : String;

action distanceFrom(a : Address) : DecimalFloat { … }

action findClosest(addresses : Address[]) : Address { … }

}

This snippet essentially defines a new object, a structured type, called Address, with five data elements and two actions.

But types are also defined using other, sometimes less explicit, constructs. For example, the query:

select name, email from Employee;

defines a new unnamed structured type with two elements: name and email, the types of which are derived from the corresponding elements in the definition of the Employee object.

Persisted objects are objects that have their own storage defined. In its simplest form, an entity maps to an underlying database table; similarly a view maps to a database view. But this isn’t mandated in the language model. We could imagine other mappings that allow for more flexibility in maintaining the system. For example, an entity may be manifested as a view over some generic table (supporting more dynamic data schemes), or even an entity defined in an OData service endpoint. The distinguishing characteristic of an entity is that its data manipulation operations are defined automatically by system (by the River compiler).

There are other types of objects in River – roles, errors, annotations and components; each defining its own unique characterizing behavior and defined using its own syntax, usually very similar to other object definitions. An overview of these is beyond the scope of this measly blog post. I encourage you to go forth and explore 😀 .

Execution – Go with the Flow

As explained previously, one of the core objectives of the River language is to decouple the created application content from specific runtime containers, at least as much as possible. The corollary of this is that the River language defines an abstract execution model, which serves as the contract between the runtime containers and the created content. In other words, it defines the contract that says: “if the content is created with that syntax, then you can expect this certain runtime behavior from the supported set of runtime containers”:

container-content separation.jpg

So for this we require at least some level of formal semantics of how data is processed – read and updated. It should be expressive enough, and at the same time abstracting away technical details of the executing system.

The Basic Model

At the most basic level, we deal with instances of objects, which are collected into collections called streams. We define a store – the representation of a persistent repository of values in our model:

Basic PL Elements.jpg

Roughly speaking, an object instance is one value of data in our model. It has a data type (possibly anonymous), and represents a single value.

A stream is a collection of objects instances, possibly infinite. It might be ordered or not, depending on the specification of the stream by the developer. Object streams appear in various ways in the application specification:

  1. Stream literals. For example:
    1. [1,2,3,4]
    2. [Employee{name: ’Jane Doe’, email:’jane@example.com’}, Employee{name : ‘John Doe’, email : ‘doe@example.com’}]
  2. As a result of expressions that compute streams. For example, queries or stream operations
  3. Modeled in the data model.

          For example:

     entity SalesOrder {

           …

           element items : association[0..*] to SalesOrderItems via backlink order;

}

     In this example, SalesOrder.items is a stream of SalesOrderItem instances, defined by an element in the model (per instance of SalesOrder).      Additionally, SalesOrder here defines the set of SalesOrder instances known in the system; this is an example of a stream defined by the name of an       entity in the model, referred to as an entity stream.

The object store (or “value store”) is what represents the application state, barring external systems. The set of values that are persisted by the application, shared between users and different flows. The values in the store are the only representation of the application state. There is no notion of memory allocation or transient vs. persistent state per se.

Flow of Actions

An action is a (named) series of statements that specify how values are  computed or changed in the store. The developer writing the application code focuses on how data is changed in the store. He doesn’t deal with making a connection to a database engine, the volume of data that gets transmitted, caching values, etc. So an action defines a series of data computations and/or updates, as well as manipulations of external systems. In other words, an action essentially computes some values, based on inputs and stored values, and chooses whether to update the application state with new values (=modify the store).

Execution happens in flows. A flow of control starts with a specific action (or error handler) invocation. This could be an outside invocation – a request made to the system, or some predefined event prescribed by the developer. An action may specify invocations to other actions that are in the same flow. A flow ends when the statements in the invoked action (the flow’s “root” action) finish execution. There’s no guarantee that the execution of a flow is necessarily sequential. It is however guaranteed to “happen” together – all or nothing.

Execution semantics therefore revolve around the data manipulations that can happen to data in the application. A developer focuses on how, and under what conditions data changes – what values get persisted, and not so much how they are persisted. In this sense the River definition language follows the functional paradigm of languages, where the code is centered on defining the necessary computation or state changes rather than how the computation actually takes place.

Note that this doesn’t exclude providing “hints” to the compiler on how to optimize some computation. For example, if a developer is aware of how the data is organized by some field, e.g. date, he can sort the data by that criterion when querying it, essentially applying an order to the computation. This is essentially leveraging domain-specific knowledge to the execution of the application.

Of course, when dealing with external systems, e.g. connecting to a web service using HTTP, a developer often needs to deal with the mechanics of the connection, and optimizing the communication itself. This is often unavoidable and sometimes desirable – systems that are beyond the optimization scope of the River compiler may require some developer knowledge on how to access these efficiently.

—-

So far, we’ve had a peek at the core model behind the language. Next we’ll explore some more mechanisms already supported by the language and infrastructure around it.

To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

Leave a Reply