On User Interface Development: appending to the event log

Recently I’ve been interested in subjects relating the idea of how to make development of software more natural to human beings.

I always thought the future of programming would’ve been centred around techniques that adhere with our “monkey brain”. Instead we’re stuck in a perpetual “here’s the new text-based language or framework” cycle.

David Harel was way ahead of us in 1988 writing great articles such as “On Visual Formalisms” where he describes ideas on how computation and the behavioral aspect of programs could be visualized in a more intuitive and graphical manner.

During this path of studies but also working on various complex UI-related projects, I’ve gathered several intuitions that seem to exist specifically in the context of UI development.

Please note that these are just my own personal intuitions on this very complex subject, and are merely described this way (as truths) to challenge the reader to think more deeply about these subjects.

Intuition 1: UI is a function of data

Libraries such as React have shown how a UI can be constructed and developed as being a function of state (or data). Components in React follow this idea quite naturally, where the inputs of a component (props) are data which is transformed into DOM elements.

More importantly the props and the rendered UIs are in sync. The complexity of developing UIs therefore should mainly be concerned with how this data is constructed. Even though some complexity exists in the development of the components themselves, I’d argue that working on how to assemble the data is a greater issue.

Intuition 2: Data is derived from an event log

Event sourcing is the concept of deriving data (or reassembling it) given an event log. This concept is quite prominent in databases. You store a temporal log of events (things that happened), ordered by time.

By replaying the events we can always derive the same state. This is the concept that Redux is based upon.

Assembling the data used for our UIs can therefore be achieved using an event log. Replaying the logs is usually done using a reduce operation.

Intuition 3: Deciding when and what to append to the event log is the most complex part of development

Now that we’ve discussed how to derive a UI from an event log, it becomes also quite apparent that each interaction of the user, and therefore each change to the UI, is actually an append operation on the event log.

Whenever we add an event to the log, we are instructing the data to change in a specific way, hence we are updating the UI.

When we program UIs, this is essentially what we do all the time. We build functions, objects, classes that append events to the log. The log might not explicitly exist and the events might be spread all over the code, but conceptually this is what happens when we code UIs.

Deciding what must, may, or must not be appended to the log is where most of the complexity lies.

Intuition 4: Finding more natural ways for deciding when and what to append to the event log is the direction we should be moving towards

In the rest of the article I’ll talk about concrete proposals for generating the event log as we go; as things happen to our UI or to the outside world.

I’ll be concentrating specifically on three different ways one can achieve this goal, namely using:

  1. conventional programming idioms
  2. Statecharts
  3. Behavioral Programming (aka Scenario-based programming)

Example

As a more concrete example to this idea of naturally building the event log, we can take an example of a simple UI with a button. When clicked the button will start an HTTP request. Once the request is resolved, we will show the user info and finally hide the navigation.

One of the main questions is: how do we append stuff to the event log as things happen in our UI? What would be the most natural way of doing this for a human being?

Our goal is to end up with an event log that looks like this:

[
BUTTON_CLICKED,
START_HTTP_REQUEST,
HTTP_REQUEST_RESOLVED,
SHOW_USER_INFO,
HIDE_NAVIGATION,
]

Generating the log using conventional idioms

A naive approach to generating such log would be to use our usual programming idioms.

UI.onButtonClick(() => eventLog.emit(BUTTON_CLICKED))
eventLog.on(BUTTON_CLICKED, () => eventLog.emit(START_HTTP_REQUEST))
eventLog.on(START_HTTP_REQUEST, () =>
startHttpRequest().then(() =>
eventLog.emit(HTTP_REQUEST_RESOLVED)
)
)
eventLog.on(HTTP_REQUEST_RESOLVED, () => {
eventLog.emit(SHOW_USER_INFO)
eventLog.emit(HIDE_NAVIGATION)
})

From the above example we are treating the eventLog as an event-emitter. There are several other ways to achieve the above but that’s not the point of the example.

One problem with this approach is that triggering a BUTTON_CLICKED will always trigger START_HTTP_REQUEST and possibly the rest of the other events as well.

What if the request is already in-flight, and we don’t want to re-trigger it?
To achieve this we’d need to keep track of some state such as a requestRunning boolean variable:

eventLog.on(BUTTON_CLICKED, () => 
if (!requestRunning) eventLog.emit(START_HTTP_REQUEST)
)

This works well enough for this example, but we are trying to push the envelope and see how we can do better.

Letting a Statechart figure out what and when to append

Statecharts go a step further than the earlier example and allow us to describe the logic for what and when to append, using a graphical representation similar to a state-machine with hierarchy.

{
Init: {
on: { BUTTON_CLICKED: 'RequestRunning' }
},
RequestRunning: {
onEntry: START_HTTP_REQUEST,
on: { HTTP_REQUEST_RESOLVED: 'Resolved' }
},
Resolved: {
onEntry: [SHOW_USER_INFO, HIDE_NAVIGATION]
}
}

One drawback of this approach as that the logic for triggering the HTTP_REQUEST_RESOLVED event lives outside the statechart. This could be however advantageous in some occasions.

With statecharts the problem of not triggering START_HTTP_REQUEST if the request is already running is solved in a very natural way: we transition to a RequestRunning state where BUTTON_CLICKED is not handled.

Statecharts are in my opinion a far superior abstraction then our earlier example. One obvious advantage is that the behavior can be visualized. With concepts such as hierarchy and parallelism we are definitely moving towards something more natural to humans.

Behavioral Programming

With Behavioral Programming we control the event log using something called a behavioral thread (or b-thread). The key idea of behavioral programming is that we can create new threads that change the behavior of already written threads.

Thanks to this idea, when our software specification changes, we don’t necessarily need to modify existing code. We can add new threads that modify the behavior of other threads.

Each thread lives on its own and is unaware of other threads, but they’re all interwoven at runtime, allowing them to interact with each-other in a very novel way.

Before we move on I’d highly recommend you read the original paper by David Harel: Behavioral Programming.

The novel part is that each thread can not only specify which event to wait for and which event to request — this was already achievable in our earlier event emitter example — but it can also specify which event to block; any thread can block execution of other threads, effectively modifying their behavior.

Here’s how it works. Following the earlier example, we define two threads below, each doing exactly what they describe:

thread(
'When button is clicked, start http request',
function* () {
while (true) {
yield { wait: BUTTON_CLICKED }
yield { request: START_HTTP_REQUEST }
yield startHttpRequest() // execute promise
yield { request: HTTP_REQUEST_RESOLVED }
}
}
)
thread(
'When HTTP request is resolved, update UI',
function* () {
while (true) {
yield { wait: HTTP_REQUEST_RESOLVED }
yield { request: SHOW_USER_INFO }
yield { request: HIDE_NAVIGATION }
}
}
)

Now comes the interesting part. We said we don’t want the BUTTON_CLICKED event to trigger the http request if it’s already running. With behavioral programming, we don’t have to change earlier-written code to achieve this.

We simply add a new thread that “modifies” the behavior of the earlier threads:

thread(
`Only start HTTP request if it's not already running`,
function* () {
while (true) {
yield { wait: START_HTTP_REQUEST }
yield {
wait: HTTP_REQUEST_RESOLVED,
block: START_HTTP_REQUEST
}
}
}
)

What’s even more interesting is that the scenario “Only start HTTP request if its not already running” is achieved with behavioral programming without the use of state.

Here’s an image on behavioral programming worth a thousand words:

Combining Behavioral Programming & Statecharts

One interesting insight is that Statecharts and Behavioral Programming can play together. With a Statechart you tell it what happened (event), and by remembering the last state you were in, it tells you what to do (action).

An action that is output by the Statechart can be fed into a behavioral program (a list of b-threads) as an event.

The output of the behavioral program can then be fed back into the statechart as an event transition.

Here’s the complete example rewritten using both a statechart and a b-thread.

{
Init: {
on: { BUTTON_CLICKED: 'RequestRunning' }
},
RequestRunning: {
onEntry: START_HTTP_REQUEST,
on: { HTTP_REQUEST_RESOLVED: 'Resolved' }
},
Resolved: {
onEntry: [SHOW_USER_INFO, HIDE_NAVIGATION]
}
}
thread(
`Start an HTTP request`,
function* () {
while (true) {
yield { wait: START_HTTP_REQUEST } // output of the statechart
yield startHttpRequest() // execute promise
yield { request: HTTP_REQUEST_RESOLVED } // send event to statechart
}
}
)

It’s unclear how to take advantage of the thread’s blocking behavior when using statecharts in this way. It does however make sense to have threads worry about actually communicating with the outside world, and have the statechart worry about state.

Conclusion

In this article I hope to have sparked some interest in thinking about UIs without all the frameworks/libraries/jargon that usually surrounds it.

We should move towards more natural ways of programming UIs, and solutions certainly exist that intuitively do a much better job than our current methods.

If you’d like to try Behavioral Programming in the context of JavaScript and the Web I’ve written a simple library that helps you write the aformentioned code: https://github.com/lmatteis/b-thread