Sketching a language for Web Programming
In this post I want to talk about a language for representing user interfaces and their dynamics.
At the moment, the language is only sketched and I would like to present the basic idea even if it is nothing but an idea.
My background is from React and Vue. So this post is influenced by them, in a positive and in a negative way. I do not know Elm, but I feel there are some similarities with what I am writing here, in particular the idea of decoupling views from updates.
The brief summary outlined below does not pretend to be complete nor accurate. Rather, I want to highlight the main points I would like to tackle with my language proposal.
An opinionated inaccurate view
Over the past years several frameworks and programming paradigms have been emerged to build Web applications.
These approaches can be very different, but they have some elements in common. Of course, reality is more complex than this summary. For example, I do not consider React Context API. However, I am not going to make a detailed comparison among different frameworks.
Component = rendering unit + bindings + lifecycle
Components are the main unit. They define how to render a bit of user interface (view) from a state and bind dom events and elements (in a Babel of different syntaxes and semantics). Usually, components have a life-cycle (e.g. mount, destroy), can be composed and allow some form of child-parent communication (e.g. props, events).
Views are defined in a declarative way
Views are usually defined in a declarative way. Programmers do not have to take care about how a page is rendered; this is different from previous approaches (e.g. Backbone) where programmers had to implement the actual rendering algorithm. There are at least two declarative approaches. The first one is based on templating languages that extend HTML syntax (e.g. handlebars) with some control statements (e.g. if, for each). In the other approach, views are defined in a native language with some primitives or functions to represent HTML tags (e.g. JSX is Javascript extended with HTML-like tags).
Inter-component communication
Historically, components lacked tools for complex inter-component communication. For this reason, several paradigms have popped up. For example, Flux is a design pattern used in many libraries, e.g. Redux. Basically, an application has a global state as a unique source of truth. Individual components map the state to views and interact with the global state sending actions/events that mutate the original state.
A more abstract approach
I want to adopt a more abstract point of view independent from the actual implementation.
First, I want to represent views with a high level data structure. For example, a view is a tree, which is part of a bigger tree we call dom; I do not care if this tree is described using templates or JSX or pure functions, how elements are bound to state or which particular algorithm I use to update the actual dom in my browser.
Then I want to abstract away from the underlying communication mechanisms and architectures. We want to describe what changes (e.g. data dependencies), not how (e.g. actions and mutations).
Finally, dom and state must be separated and, above all, independent.
The last point is inspired by bigraph theory [1]. Bigraphs are a formalism to model space and motion. I wrote a short introduction here. The underlying insight is that space and motion should be modeled by independent data structures.
In an analogous way, we might think of a web application as a spatial part (dom) plus a dynamic part (communication). These parts are logically independent. For example, I should be able to click on an “add to basket” button and see the navbar basket updated regardless on where the button and the basket are located in the user interface.
As a consequence, we do not need any special device for child-parent comunication. There is only one uniform communication mechanism independent from the dom hierarchy.
Another aspect is state. In modern web development state is seen with suspect. We are told to avoid side effects and there are good reasons to follow this generic advice. However, I think, the problem is not state in itself, rather, the lack of compositionality of states. React and Vue are good tools to compose user interfaces (i.e. doms), but, in my opinion, they lack compositionality mechanisms for state (maybe Context API could change this).
In the rest of this post, I will sketch an operational semantics and I will borrow several ideas from the operational semantics for signal flow graphs [2]. Please bear in mind that many important details are omitted and I am not giving a full working set of operational rules. All of this is very work in progress. My main goal is to communicate the informal intuition to the average programmer who does not know anything about operational semantics and category theory.
Abstract dom
An abstract dom is a representation of possible dom states. We prefer the term abstract dom, because virtual dom is quite overloaded at the moment.
An abstract dom is defined by three elements,
- Nodes
- IF Nodes
- EACH Nodes
Nodes are standard and custom dom element such as div
, span
and so forth. IFs and EACHs are control nodes for conditional and iterative rendering, respectively.
In other words, an abstract dom is a dom with some extra features. We could add recursion and other fancy stuff to the list of extra features, but, for now, we prefer to keep it simple. In practice, we define templates or jsx with IFs and EACHs. Rarely, we need more power. However, this is not a good reason to not add more power.
A node has some ports and an inner state. Ports are like arguments of a function (even if their relation is not necessarily functional). Inner state is the state of the node. For example, a button can be pressed
or unpressed
or an li
element can be parameterized by its position $i$ within a list.
The behavior of individual nodes is defined by a set of rules. A rule is a labelled transition of the form $t \overset{l}{\rightarrow} t’$ where $t$ is the redex, $t’$ is the reductum and $l$ is a label.
For example, consider we want to define the behavior of a button. In the picture below we define a node for the button element and two rules that simulate the click event in HTML.
A button is represented as a box with the name of the tag (i.e. button
) and an inner state (i.e. pressed
or unpressed
). A button has two ports. Intuitively, one listens to dom events while the other is an event handler. We label ports with numbers in order to distinguish them.
A transition label is not a single symbol, but a set of symbols corresponding to the possible values on ports. Instead of adding labels on the top of arrows, we put labels on redexes. Not all ports must have a value in a transition and the assignment is not necessarily functional nor deterministic.
The first rule R_BTN_CLICK
says that, if a button is unpressed
, when it gets a click event on port 2, then it changes its state to pressed
.
Instead, if the button is already pressed
and port 2 does not have any click event (e.g. user has released the button), then the button state gets unpressed
again and a 1 is emitted on port 1. This is rule R_BTN_RELEASED
.
As for bigraphs, nodes can be nested. On the right hand side of the picture above, our button is inserted into a div
element and has a sibling span
node. Note that port links are independent on the dom hierarchy and cross the boundaries of the parent element.
Control nodes are like dom nodes but with special rules. Let’s start with IF.
On the right, we represent the IF node while the two IF rules are on the left. A IF node is a box (we use thicker borders to denote control nodes), with a port (the IF condition) and two holes. As we will see later, every node can have holes that can be filled by other nodes (again, this is from bigraph theory). We use dashed boxes to denote not active areas. Intuitively, a sub-tree within a dashed box is not displayed and its connections are not active. In the case of IF nodes, hole 0 is the “then” branch while hole 1 is the “else” branch. The two rules are called R_IF_FALSE
and R_IF_TRUE
, respectively, and are fairly trivial.
State
We denote a state as a node with some ports and a value. State ports have a different color (i.e. red) because we want nodes communication to be mediated by state. In other words, we do not want to connect two dom nodes directly. However, I am not sure about this choice (which is influenced by the use of edges in bigraphs) and it could change in the future.
A state is not just a value container. It is a value plus some rules to mutate this value. For example, the following picture shows some examples of state and state rules.
Rules can leave the state unchanged like in lt
(R_LT2_TRUE
and R_LT2_FALSE
) or filter
(R_FILTER
) or change the state like in inc
(R_INC
). R_INC
increases the stored state by one. R_LT2_TRUE
and R_LT2_FALSE
check if a value is less than 2 and return true
or false
, respectively. R_FILTER
allows only some values to pass, in this case, only values equals to the state.
A counter example
Now we are going to put together the building blocks described in previous section. We are going to build a simple counter that shows the number of button clicks and, if this number is greater than or equal to 2, the counter explodes.
The Counter
The picture above represents our counter. We need two states for increasing the counter value and checking if the count has breached the threshold. Box nesting resembles the structure of the HTML page very closely.
First click on button
Here, we show the transitions triggered when user clicks on the button once. The first transition is the application of the rule R_BTN_CLICK
: when user clicks, the button state is changed to pressed
and that is it. Here and in what follows, we mark red the parts changed in a transition.
The second transition is more interesting. When button has state pressed
, the only possible rule we might possibly apply is R_BTN_RELEASED
. However, we need to check if we can apply this rule in a consistent way with other rules. In particular, button port 1
(right-bottom) has label 1
. This label is shared with inc
state and R_BTN_RELEASED
can be applied only if we can apply a rule for inc
where port label is also 1
.
In this case, the rule is R_INC
and we can follow the same reasoning for lt2
and apply rule R_LT2_TRUE
.
Again, this is just the basic idea. Several details must be clarified or fixed. We would like define a composition mechanism for individual rules with a sort of operational semantics in the fashion of [2].
Second click on button
Then user can click on button once more. The first transition is the same as before. The second transition triggers R_IF_FALSE
. So abstract dom changes, the then
-branch is disabled and the text BUM!
is displayed.
Note that here we are overlooking several important details. For example, we should specify better what we mean when a dom region is disabled and how the corresponding “links” can be disabled, too (e.g. it should not be possible to click on the button again).
Coming soon
In some next post, we will add more details. In particular, I want to show:
- Each rules: some nodes can be rendered iteratively and dynamically. Think about
li
elements in aul
list. - Composition: states and doms can be glued together. Building blocks are components, a mix of doms and states.
- A TODO app: a more complex example to show how to build something real (or almost real).
Credits
Diagrams in this post are created using Mathcha.
References
[1] Robin Milner. The space and motion of communicating agents. Cambridge University Press, 2009.
[2] F. Bonchi, P. Sobocinski, and F. Zanasi. Full abstraction for signal flow graphs. ACM SIGPLAN Notices. Vol. 50. No. 1. ACM, 2015.