Skip to content
This repository has been archived by the owner on Apr 5, 2022. It is now read-only.
/ thesis Public archive

Bachelor Thesis in Applied Computer Science about Composition of UI elements in reacl-c using Monads.

Notifications You must be signed in to change notification settings

marlonschlosshauer/thesis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

Composition of UI elements in reacl-c using monads

./images/hs-og-logo.png

Acknowledgment

I’d like to thank some of the many people that supported me while I wrote this thesis. First of all I’d like to thank my advisor, Markus Schlegel, for not only providing many of the ideas found in this thesis, but also sharing his vast knowledge of function programming and reacl-c. I’d also like to thank the Active Group GmbH for making this thesis possible. I’m very thankful and happy that I got to explore such an interesting topic. Lastly I’d like to thank my professor, Prof. Dr. Stefan Wehr, for introducing me to the Active Group GmbH and also giving helpful answers to my questions.

Eidesstattliche Erklärung

Hiermit versichere ich eidesstattlich, dass ich die vorliegende Arbeit mit dem Thema

von mir selbstständig und ohne unerlaubte fremde Hilfe angefertigt worden ist, insbesondere, dass ich alle Stellen, die wörtlich oder annähernd wörtlich oder dem Gedanken nach aus Veröffentlichungen, unveröffentlichten Unterlagen und Gesprächen entnommen worden sind, als solche an den entsprechenden Stellen innerhalb der Arbeit durch Zitate kenntlich gemacht habe, wobei in den Zitaten jeweils der Umfang der entnommenen Originalzitate kenntlich gemacht wurde. Die Arbeit lag in gleicher oder ähnlicher Fassung noch keiner Prüfungsbehörde vor und wurde bisher nicht veröffentlicht. Ich bin mir bewusst, dass eine falsche Versicherung rechtliche Folgen haben wird.

Offenburg, 30.03.2022

Marlon Schlosshauer

Abstract

As the need for digitalisation of workflows grows, so does the importance of the tools that we use to create them. The composition of UI elements has been a necessary tool in the process of creating UIs and is, as a result of that, well supported in modern frontend developments. We however propose that composition of UI elements can be split into parallel and sequential composition. The former is what we commonly understand when we talk about composition of UI elements: The ability to say which elements belong together on the screen, at the same time. The latter is a different kind of composition that allows us to express in which order those parallel compositions are shown, much like a wizard in a UI does. This is significant because workflows are made up of individual steps that have a predetermined order to them. However, the current support to build and maintain these kinds of structures, despite that importance, is poor. In this thesis we will examine the difference between parallel and sequential composition, go into detail about this type of composition is important, look at examples of current support for sequential composition (or lack thereof) and explore a developer friendly API for it. Furthermore, we will introduce reacl-c, a framework that enables high composability of UI elements and embraces functional programming, that is based on React. We will take a look at how composition is done in reacl-c and explain the advantages of its unique state management. We will then extend reacl-c by implementing the previously proposed API in it, therefore giving it sequential composition. At the end we give examples of sequential composition using our implementation and discuss possible improvements. With this thesis we have explained what sequential composition is and delivered a working implementation for it.

Zusammenfassung

Mit wachsendem Bedarf an der Digitalisierung von Arbeitsabläufen steigt auch die Bedeutung der Werkzeuge, welche wir für die Entwicklung dieser verwenden. Als ein notwendiges Werkzeug hat sich die Komposition von UI-Elementen beim Erstellen von UIs, herausgestellt. Deswegen wird diese auch in modernen Frontend Frameworks wie React und Angular gut unterstützt. Wir schlagen jedoch vor, dass die Komposition von UI-Elementen in eine parallele und sequenzielle Komposition unterteilt werden kann. Ersteres ist, was wir allgemein unter Komposition von UI-Elementen verstehen: Die Fähigkeit auszudrücken, welche Elemente gleichzeitig (parallel) auf dem Bildschirm angezeigt werden. Letzteres ist eine andere Art der Komposition, welche es uns ermöglicht, die Reihenfolge der zu anzeigenden (parallelen) Kompositionen zu bestimmen. Wie es ein Wizard UI Element tun würde. Dies ist wichtig, da Arbeitsabläufe aus einzelnen Schritten bestehen, die eine genaue Reihenfolge besitzen. Allerdings ist die derzeitige Unterstützung für den Aufbau und die Weiterentwicklung dieser Art von Struktur trotz ihrer großen Bedeutung sehr gering. In dieser Bachelorarbeit werden wir die Unterschiede zwischen paralleler und sequenzieller Komposition vorstellen, die Wichtigkeit von sequenzieller Komposition veranschaulichen, sehen wie momentan diese Art von Komposition implementiert werden kann und eine entwicklerfreundliche API dafür entwerfen. Darüber hinaus stellen wir reacl-c vor, ein auf React basierendes Framework, das eine hohe Kompositionsfähigkeit von UI-Elementen ermöglicht und Konzepte der funktionalen Programmierung verwendet. Wir werden zeigen, wie die Komposition in reacl-c erfolgt und erläutern die Vorteile der einzigartigen Zustandsverwaltung in reacl-c. Danach werden wir reacl-c mit der von uns zuvor vorgeschlagenen API erweitern und damit sequenzielle Kompositionen ermöglichen. Am Ende geben wir Beispiele für sequenzielle Komposition unter Verwendung unserer Implementierung und sprechen mögliche Verbesserungen an. Mit dieser Arbeit haben wir das Konzept der sequenziellen Komposition erklärt und eine funktionierende Implementierung dafür bereitgestellt.

Introduction

Components are an important tool in modern frontend development. In modern frameworks like React and Angular we can define something as a component to reuse it multiple times in our applications, encapsulate some style with some functionality or collect multiple elements into a single component. To be more precise, we can compose things in parallel because we can group elements into something that is to work together. An example of a parallel composition would be two input fields and a button, which together define a sign-in component, which can be seen in figure 1.

#+CAPTION[Example of parallel composition of a sign-in component.]: Example of parallel composition of a sign-in component. The first composition is the result of adding a button and two inputs. We further compose the first composition by adding a headline. That result is then again further composed by adding a logo.

./images/parallel-composition-highlighted.png

However, modern frontend applications have gone beyond just serving static HTML. Websites often act as a digitalization of a workflow, where the order of some steps, as in which component is shown after another, is crucial. This is also a kind of composition. Instead of composing things in parallel, we’re composing sequentially here. That is to say, we say which steps we have and in what order they appear. Together they form a sequence. It is important to point out that the here mentioned sequentiality is different from how we might know it from imperative programming (IP). In IP, sequentiality is used, even if the domain doesn’t demand it, because of its writing style. We could call this accidental sequentiality, as it is only a by-product of language design and history, rather than a conscious engineering decision. Because our sequential composition exhibits natural sequentiality (e.g first step A, then step B) we also want to represent that in our code. An example of a sequential composition would be to first ask the user for their email address, then, after having the user enter a valid email, show another input field, where the the user can enter a code that was sent to them, using their email. Notice that the second step can depend on information that was gathered by the previous step. This example can be seen in figure 2.

#+CAPTION[Example of sequential composition of a register component.]: Example of sequential composition of a register component. The first composition combines two steps, before being again further composed by adding a third step.

./images/sequential-composition-highlighted.png

Because of this dependency, composing sequentially isn’t just an optimization. It is a fundamental building block of how an application is supposed to operate. Just like how we need the ability to show both an input field and a button at the same time (to confirm the input), we also need the ability to model what happens after (and possibly before) that button has been pressed.

But despite that need, the actual support for sequential composability by modern frameworks is either non-existent or very poor. While some of the desired result can be achieved by making clever use of parallel composability or using traditional links, most aren’t truly sequentially composable and both suffer from multiple issues, some of which are:

  • They don’t yield things which we can compose further (into more sophisticated workflows)
  • Reusing these components leads to a lot more boilerplate
  • It produces code that becomes difficult to understand at a glance (or at all)
  • Logic for advancing steps (and bookkeeping) live besides the parallel composing logic
  • Bookkeeping is scattered throughout the codebase

There are more issues. The inability to easily test just the order of the components and the huge time cost required to refactor constructs of this nature are just the starting point.

Our applications already are a collection of parallel compositions. If we add the ability to compose sequentially, we gain another tool to control our programs. Not only would our code be more expressive and more concise, bugs could also be reduced because the resulting systems represent more truthfully what they set out to do, thanks to the provided API of the framework. Most importantly, we could build workflows out of smaller flows or other workflows entirely and share these across our applications, just like how we do with our UI elements. Given the lack of support and the possible applications it is of high interest to find a way to compose sequentially, easier.

Before we talk about our implementation of sequential composition we will first introduce reacl-c, the framework in which we implemented it. Then we will look at the current way of implementing sequence like behavior, before exploring a possible API for sequential composition. Afterwards we will showcase how we implemented our API in reacl-c. At the end we will both give examples of our API and discuss potential improvements. Knowledge of composition of UI elements in modern frameworks like React or Angular is helpful but not required. Neither is knowledge of reacl-c. Some basic knowledge of reacl-c’s language, ClojureScript (or Clojure) is required (LISP syntax, basic structure, variable definitions, let usage, destructuring).

Explaining Frameworks And Tools

To best understand how our sequential composition might work, we first need to get familiar with what the tools that we use have to offer. We will start by looking at React to better understand it and how modern UI development is done. Afterwards we will learn what reacl-c is and what important features it has that will help us implement sequential composition. At the end of this chapter we will take a look at monads and why they’re also important for our sequential composition.

What is React

React is a popular UI framework developed by Meta (formerly Facebook) and is written in JavaScript. It is important for us to understand, because the framework that we’re going to work in, reacl-c, is based on React and as a result inherits a lot of its ideas. React, like many other modern JavaScript frameworks, is a Single-Page-Application (SPA), which means the entire application is loaded and available after visiting a single page. Instead of having each page defined in HTML, developers are able to define all pages in JavaScript, using Reacts JavaScript Syntax Extension (JSX). React takes those JSX definitions and builds HTML accordingly, before adding it to the browser’s DOM. An example of JSX can be seen in listing 1, which shows how the register component shown in figure 1 could be implemented in React. #+CAPTION[Defining the register component shown in figure 1.]: Defining the register component shown in figure 1. JSX allows developers to work with HTML and JS in the same file. Note the ability to call previously defined components, like they’re HTML elements (lines 17, 26).

const registerInputs = () => {
    const [email, setEmail] = useState();
    const [password, setPassword] = useState();
    return (
	  <div className="register-inputs">
	    <input name="email" type="text" value={email} onChange={(e) => setEmail(e.target.value)}/>
	    <input name="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)}/>
	    <button>Register</button>
	  </div>
    )
}

const registerContainer = () => {
    return (
	  <div className="header-container">
	    <h2>Example page</h2>
	    <registerInputs/>
	  </div>
    )
}

const registerPage = () => {
    return (
	  <div className="register-container">
	    <img alt="Company logo" src={image}/>
	    <registerContainer/>
	  </div>
    )
}

An advantage of a SPA design is that commonly used components, like a header and footer, can be shared over multiple pages without having to be retransmitted over the internet, as the JavaScript code is still in memory inside of the client’s browser. In such a system navigation does not mean the browser is navigating to another page by doing a HTTP call, but rather it tells React to build the desired next page from the JSX definitions already present on the client. Besides requiring less bandwidth on successive visits, this also leads to better response times on page visits after the initial load, as those pages are also already in memory. The downsides are that the client needs to compute the page by executing JavaScript to build it and the longer initial load times associated with this computation.

Another important feature of React is that the developer must explicitly declare the state of a component. This is used to improve performance, as only components which are affected by a state change are redrawn upon changes. Old components remain untouched. This also helps with identifying where state is and how it might be changed in the future.

React is also a popular basis for other frameworks like Preact. It is especially popular for ClojureScript based frameworks. Notable examples are om, reagent, rum and as mentioned earlier, reacl-c. The next chapter will focus on the latter, reacl-c, and showcase both syntax and important features.

What is reacl-c

In this chapter we will talk about the benefits of reacl-c, give an overview of some of its API and explain the most important features. We will also implement parts of the previously in figure 2 shown registration example.

reacl-c is a UI framework for ClojureScript that is designed to make the composition of UI components easier, by giving developers lots of tools to both manage components and their state. Like other ClojureScript frameworks, reacl-c wraps the previously introduced React, to best take advantage of Clojure’s immutable data structures. Like React, reacl-c allows the definition of components, called Items. These Items can have child Items, thus forming a tree, just like components do in React.

To make composing of Items easier reacl-c takes a different approach to state than React. There are two types of state. Local state, which is defined inside of the component and outer state, which is implicitly passed down from the parent to the child. The latter has the unique feature of putting the emphasis on the parent. We will explore why this is important shortly.

Another key difference to React, is that Items can emit actions if an event occurs inside of them. These actions propagate upward the item tree. Every item can register an action handler, which captures the action and reacts to it. This enables the developer to define a (child) item completely independently from their potential future parents, leading to more reusable and more composable components. Instead of having the parent pass down a function to, e.g manipulate state, the component can emit an action upwards and trigger the same functionality in the parent. Another benefit is that the parent component can adapt the output of the emitted action further. Figure 3 shows the inversion of control by comparing emitting of actions to passing down callbacks. Each circle represents a component. To communicate the callbacks need to be passed down. With actions, the children can speak up on their own.

./images/emit-vs-callback.png

As an example: A button toggles an option flag from true to false and back. The state for this option is put into the parent, our button is a toggle component which will be reused across the application. In React, the button needs to receive a function which to callback, after the button has been pressed. In reacl-c the button does not need to receive anything from the outside world, as the button emits an action when pressed, which the parent can capture and act upon. See listing 2 for an example of a button that works like that, in reacl-c. #+CAPTION[Creating an abstract-button.]: We create an Item called abstract-button which contains a button. If pressed, it’s going to fire an :action with the value :pressed to its parent. Notice the lack of callback given to our abstract-button component. text is the only parameter given, yet our component will be able to communicate with a parent thanks to the action system.

(defn-item abstract-button [text]
  (dom/button {:onClick (fn [] (core/return :action :pressed))} text))

The action system in reacl-c is inspired by the functional programming concept of effect systems. An effect system allows code to express effects, by tracking them directly through the type system cite:&effect-systems-in-haskell. Like an effect system, the action system in reacl-c allows us to express these effects by returning either the Action or Return type.

With this style of communication, only the parent needs to know the child. The developer does not need to tell the child where to send the action. Reacl-c takes care of that work for us.

To make components even more composable the parent can not just control the result, it can also control which state is given to its children in the first place. The parent can therefor handle the child Items like they’re pure functions, that together with the parent compose to a new Item.

In React often another package like Redux is used to fix the problem of having to pass-down functions. Instead of saving all state in each component, state can be managed at a central point which components can send messages to, causing the central state to change. This can cause issues with the composability of components. The problem with this approach is that components cannot be placed multiple times into the app, without making sure they don’t all work on the same central state, first. Reacl-c solves this issue by allowing any component to send and receive messages. Not only does this allow for local reasoning, but it also enables us to wrap a component with an action handler and place it infinitely in our app without worry, as the component won’t affect anything outside of that handler (as long as the handler is setup correctly).

Before we dive deeper into the unique features that reacl-c has to offer, we’d like to give an overview of the most important functions in the form of table 1 and table 2. Table 1 shows their signatures while table 2 gives a brief description. We will explain each function further in the coming chapters.

Take note how many of those functions take something and an Item as a parameter - and return an Item again. This is what allows us to build concise and powerful components that we can also easily compose further.

Having now seen the most important functions, we’re ready to learn how to use these basic building blocks. Let’s begin by talking about how we can use and define Items.

NameSignature
core/local-statex -> Item -> Item
core/dynamic([o, i]) => Item -> Item
core/handle-actionItem, ([o, i]) => Returned -> Item
core/return:action -> x -> Returned
core/focusLens -> Item -> Item
dom/div[Item] -> Item
NameDescription
core/local-stateMakes the first parameter available as inner state inside of the second parameter.
core/dynamicGives access to state inside of function. Function needs to return an Item.
core/handle-actionCatches actions emitted by first parameter. Calls second parameter when caught.
core/returnReturns something that can either trigger an emit of an action or a state change.
core/focusRestrict state of the second parameter to only what the first parameter (lens) allows.
dom/divBundle multiple Items into one Item (just like a <div> would do in HTML).

How an Item is made

Reacl-c is made up of Items and functions which operate on these Items. Much like in React, we can use these Items to build our components. The dom namespace offers all the necessary HTML elements in the form of Items (e.g dom/div, dom/button). We are however not limited to HTML or even visible elements. It is also possible to place empty Items to cause effects (e.g HTTP request). As an example, to create a headline all that is needed is (dom/h2 "Text"). To make more complicated Items, combinators like (dom/div) or (core/fragments) can be used. These can contain multiple Items. See listing 3 for an example. #+CAPTION[Showcasing composition of multiple Items.]: Showcasing composition of multiple Items into one by using a div, by building a static version of the previously in listing 1 implemented registerInputs component.
(def register-inputs
  (dom/div
   {:class "register-inputs"}
   (dom/input {:name "email" :type "text"} "email")
   (dom/input {:name "password" :type "password"} "password")
   (dom/button "Register")))

Behind the calls to the dom namespace are Item constructors, which can also receive a ClojureScript map as their second argument. With this map things like CSS classes and inline-style can be applied. We saw this used in listing 3 in line 3 to add a class to our div. If the Item is interactive, like Buttons and Inputs are, the keywords onChange and onClick can be used to register a callback (that can change state).

There are other functions which, much like div, don’t add something visually but change the behavior of the Item. Functions like core/focus, core/dynamic and core/handle-actions, to just name a few. We will take a closer look at each soon. A working example of one of the components in the registration sequence of figure 2 can be seen in listing 4. We will use this listing to examine more of reacl-c’s API.

#+CAPTION[A complete example of the first step shown in figure 2.]: A complete example of the first step shown in figure 2. Previously defined in listing 3 but without state or any form of interactivity. Now thanks to core/local-state, core/dynamic and core/return the Item has state that is manipulated by user input.

(core/defn-item login-information []
  (dom/div
   (dom/h2 "Login information")
   (core/local-state
    {:email ""
     :password ""}
    (core/dynamic
     (fn [[outer inner]]
	 (dom/div
	  (dom/input
	   {:name "email"
	    :type "text"
	    :onChange
	    (fn [e]
	      (core/return :state [outer (assoc inner :email (.e target value))]))}
	   (:email inner))
	  (dom/input
	   {:name "password"
	    :type "password"
	    :onChange
	    (fn [e]
	      (core/return :state [outer (assoc inner :password (.e target value))]))}
	   (:password inner))
	  (dom/button {:onClick (core/return :action inner)} "Confirm")))))))

We now know how Items are created. However, our knowledge is limited to static Items that never change. To change that we will look at how state works in reacl-c. Afterwards we will examine how we can make our components interactive by handling actions.

State Management in reacl-c

Like with React, handling state is very important. Thankfully reacl-c gives developers many ways to tackle the problem of state management. Also like React, developers can easily make out if a component is using or changing state, which makes working with state easier.

Previously we saw how we can use State in React with the useState() function. State in reacl-c is a bit different and can be shared in multiple, more complex, ways.

Firstly, while a component might have state, it is not accessible to the developer until they use the core/dynamic function or core/with-state-as macro. This has the benefit of instantly marking a component as one that needs and works with state. We will take a look at what the two functions do shortly.

Secondly, like mentioned earlier, state is split into two categories:

  • Inner state, which is defined inside of the component by using either core/local-state (see listing 4, line 4-6) or core/isolate-state.
  • Outer state, or state that is passed down from the parent component.

That passing down of outer state happens implicitly. That means we don’t need to tell the parent to share its state with its children.

This can at first seem strange, but the intention becomes more clear by looking at the following example: If we write (inc (+ 3 4)) we specify that the result of (+ 3 4) should be passed to (inc ...), but we don’t do so in a verbose way. Rather it is implicitly done because of the structure of the code. We know that having functions nested inside of each other causes this kind of passing of return values. ClojureScript developers are already familiar with this, as it is arguably the most important feature of the language.

Let’s apply similar thinking to state and Items in reacl-c. Take a look at the following: (div (core/local-state {:email ""} (login-information))). core/local-state adds the first argument as (inner) state to the second argument (the Item). We don’t have to explicitly say that (login-information) should inherit the state (e.g {:email ""}) from the parent. The structure indicates this relationship.

If we take this further it becomes clear why it is similar to the inc example. If (login-information) has access to :email, it can then change it. If it does, it is like login-information returns the result of some computation to the parent (because the state belongs to the parent), where it is then used for further computation. After all, we placed login-information in the parent for a reason. In this case to return something to us. Just how we placed (+ 3 4) inside of the (inc ...) to return something, as well. In both cases we didn’t specify that relationship in a verbose way (e.g like with a let), but rather it was implicitly known to the developer by the relation. It’s worth noting that the parent retains full control of what state is shared, as it can use lenses (with the core/focus function) to block some of its state from being shared with its children.

It does not only lead to less code that needs to be written (because of the absence of all the wiring) but also has an effect on composability, because now children can be placed within anything. State is implicitly fed into Items and changes automatically find their way up to the parent (and their parent, and the parent of that parent etc.).

If state inside of the parent isn’t in a format that is useful to the child, it can be further composed by wrapping it in another component that adapts the parent state by transforming it into the correct format. This is again possible because state is also implicitly passed into the adapter and from the adapter into the actual child. We will explore this mechanism in-depth later.

Let’s apply our knowledge of outer and inner state and take a closer look at how we can use the provided API, by taking listing 4 as an example. We can see the previously mentioned core/local-state (line 4-6) is used to add a map with two fields as inner state. The second argument (line 7)is an Item which the inner state will be applied to. But, just core/local-state isn’t enough, as we don’t have a keyword to access our inner state. To access that newly added inner state, we need to use the core/dynamic function. The function itself takes a single parameter: Another function, that now must return an Item. That function gets the surrounding state as an argument (line 8), therefore anything inside of it has at least read access to the state. The parameters of that function have been destructured into outer and inner state (line 8). We can see that the function will return a core/div and that the provided inner state is used in lines 15-16 & 22-23, 24.

But we can do more than just access state. With the core/return function we are able to change state (and send actions, see next chapter). We can see usage of core/return in listing 4 at lines 15, 22 & 24. The core/return function takes two arguments. A keyword and a value. If the :state keyword has been supplied (like in lines 15, 22), we will set the state to the second argument (the value). This will change both inner and the inherited outer state. In line 15 we can see that the :email keyword in the inner state is to be updated to the value of the event, while the outer state is kept the same. Outer and inner state are supplied through a list, which is why we also return a list as our value which the state will be set to.

Now we know how to both access and update state. These are obviously important features of reacl-c. Next we’re going to look at how components can interact with each other by using actions.

Emitting and handling actions

Both core/handle-actions and core/return allow us to work with actions. Actions are an important tool for children to communicate with their parents. Like the implicit (or explicit) passing of state from the parent, this feature makes our Items even more composable. core/return was previously introduced as a mechanism to set state, but it can also be used to emit actions by using the :action keyword (instead of :state). This can be seen in listing 4 in line 24, as a result of user input (button is pressed). The emitted action from the button press will then travel upwards until it is caught by a core/handle-action. Like shown in table 1, core/handle-action takes two parameters, an Item which to catch actions from and a function, which will be called once an action has been caught. The function has two parameters. First the state (which can again be destructured to [outer inner]) and a message, which will be the emitted action.

Take a look at listing 5 to see our previously in listing 4 defined login-information component being wrapped by a core/handle-action. First we define some local state (lines 2-3) to keep track of if the button has been pressed. Then inside of that local state we call core/handle-action (lines 4-11) and give it both an Item (in the form of a core/dynamic) and a function (lines 10-11). We see that our function sets :login-info inside of our inner state to the result of the action (line 11). This will cause a redraw of our component (because of the state change) and will hide the login-information component (lines 7-9). #+CAPTION[Using core/handle-action to catch actions.]: Wrapping login-information with core/handle-action to catch actions and change local state accordingly.

 (def registration
   (core/local-state
    {:login-info {}}
    (core/handle-action
     (core/dynamic
      (fn [[_ inner]]
	 (if (nil? :login-info)
	   (login-information)
	   (dom/h2 "Login info received")))) ;; TODO: show next step!
     (fn [[outer _] ac]
	(core/return :state [outer {:login-info ac}])))))

The ability to communicate between components, without having to explicitly build that connection, is something that will aid us massively when building our sequential composition. Much like with the parallel composition, we don’t have to worry about wiring that might turn out to be messy.

What is a monad

We’ve previously looked at frameworks, to get a feeling for what paradigm we’re in and which tools are available to us. We’re now going to look at a programming concept called monads and explain why it will be important for our sequential composition.

Monads are often described as the programmable semicolon, because they allow us to describe what happens once an operation, that uses a monad, is done. This is helpful, because we can abstract away difficult logic, so that the developer can continue working with our complex types, as if they are primitive types. There are different kinds of Monads that serve different purposes. One use case for some monads is to allow us to chain operations on often abstracted away types. This is done to transform data or control the flow of the program or both. In order to allow for sequential composition we need to make use of both, with a heavy focus on controlling when and what is executed and shown. We’re now going to show a small example of a monad, before talking about what needs to be done to be considered a monad. Afterwards we will end this chapter by talking about why monads are important for our sequential composition.

The Maybe Monad

A popular monad is the Maybe type in Haskell. The language doesn’t feature a null value, instead we can use Maybe to express when a function might return Nothing or Just of something (e.g. Just 5). Because this type is a monad we can easily chain it together. This allows us to combine multiple operations that might fail and stop execution in case any of them do cite:&graham-monads. See listing 6 for an example of multiple HTTP operations that all may fail. All we need to worry about is working with the actual type. The responsibility of working with the side-effects is taken away from us.

#+CAPTION[Showcasing the Maybe monad.]: Instead of having to manually check if each operation succeeded, thanks to the Maybe type and >>= operator, the chain will stop if any of the calls return a Nothing.

getUserById "df743aec" >>= getTeamByUser >>= getTeamManagerByTeam >>= getSalaryById

Monads are everywhere and most developers will have used them, even if they didn’t know what a monad was at the time. They help us write cleaner code that is easier to share and make API’s easier to use, because they’re composable. Common cases for monads are IO operations, handling of errors, UI work and to establish a context of values.

What is required to be a monad

To be of the monad typeclass the type needs to provide two functions and satisfy three rules cite:&haskell-monads. The required functions are >>= (also called bind) and the return function (sometimes called pure). A bind takes an instance of a monad M and a function that gets a value a and returns an instance of type M with a inside of it. The result will be a monad M again. This is what enables us to chain these operations together. The second function, the return, takes a value a and returns a monad M with value a. As an example Just 1 works like a return, in that we give it a 1 and it gives us a Maybe (with the value of 1 inside of it). To better understand the signature, see listing 7 where Haskell notation has been used to spell out the types.
(>>=) :: M a -> (a -> M b) -> M b
return :: a -> M a

>>= takes a monadic value and a function that takes a value and turns it into a monadic value. The result is another monadic value. We can think of this as taking the value out of M b, so the function can work with it and transform it to something different. The result is another monadic value, so it can be composed further. The return function is what builds a monadic value from a primitive value. An example of a return would be the Just in front of Just 5, that turns a 5 into a Maybe value (of Just 5).

An implementation of these functions needs to fulfill the following three rules to be considered a monad cite:&haskell-monad-laws:

  • Left identity: return a >>= h = h a
  • Right identity: m >>= return = m
  • Associativity: (m >>= g) >>= h = m >>= (\x -> g x >>= h)

Left and right identity are tests to make sure the types work out correctly. return can both be called with a value to create a monad, when provided on the left side of the bind, or be given as a continuation function, if provided on the right side. The rule of associativity tests that the order of operation remains, even if the association of the operations changes. Both (A >>= B) >>= C and A >>= (B >>= C) should yield the same result.

Why we use monads for our sequential composition

Using monads makes sense because they allow us to abstract the actual logic (e.g. waiting for an action to be emitted) away from the developer while providing strong tools to combine our sequential steps. The required functions also are a great fit for our API. With the >>= operation we give the developer an easy way to further compose their sequential components. We also need to make a new type, which we can in turn use to distinguish our sequential and parallel compositions from each other. The return function then acts as a constructor for our new type.

We will take advantage of reacl-c’s great action system to keep necessary wiring of steps to a minimum. As a result we will have to wait for actions to be emitted. Because of that (and the inherent domain of the problem) we need to take asynchronicity into account. That means our sequential composition needs a specially labeled environment in which it can be executed. This is much like monads in Haskell too, where monads can be executed in a do block. The next chapter will talk about the current and possible developer experience for our sequential composition. No doubt will monads allow us to create a better, frictionless API to improve how developers can compose sequentially.

Building sequences

Having had both an introduction to sequential composition and some of the modern frameworks that we can use to build our applications, we now can take a look at how sequential composition is done. Or rather, how it isn’t. First we will see different approaches to building sequences (structures with multiple steps) and then we will explore how we might want to compose sequences in the future.

How we build sequences right now

Composition is supported in both reacl-c and other frontend frameworks like Angular or React. However, this is limited to creating a new component that just displays all composed components at the same time. In other words, it is only possible to compose in parallel. Currently, to create a component which initially displays some component and later changes to display another, after a certain event has been reached, the logic doing the change from one to the other component needs to be implemented by hand. In the following we’re going to examine some possible ways to implement a sequence of steps. While we will showcase reacl-c, we want to mention that any shortcomings mentioned aren’t exclusive to reacl-c, rather they are inherited from React. Other frameworks like Angular suffer from the same issues, also.

If statement

The most trivial way to switch between one of two steps is to use an if statement. While not offering composability, a simple if statement is highly effective for binary choices. An example can be seen in listing 8, where we implement a guard for the second step shown in figure 2. The first step, login-information (defined in listing 4), is shown until it emits its inner state. From here on we assume that the component verification-code exists and works identical to login-information with the exception of requiring an email string from the previous step. Same is true for personal-information but without the string requirement.

#+CAPTION[Using an if as a guard.]: An example of using an if to guard a step from being shown before another is done. This example combines the first and second step shown in figure 2.

 (def login-info-and-verification-code
   (core/local-state
    {:email nil}
    (core/handle-action
     (core/dynamic
      (fn [[_ inner]]
	 (if (nil? (:email inner))
	   (login-information)
	   (verification-code (:email inner)))))
     (fn [[outer inner] ac]
	(c/return :state [outer (assoc inner :email (:email ac))])))))

A construct like this has already been shown in listing 8, to showcase the handling of actions. Now the focus is on the if and how it can be used to give a binary choice of what is displayed. Like mentioned earlier, this way of managing steps is limiting. It is however also possible to use composition to enable more than just two steps, by using a component as the second step, that again contains an if structure like this one. And the second step of that if could again be another if structure. While functional, readability of the code suffers and keeping track of the entire sequence is challenging, as it can be spread out through the entire codebase. Also, gaining access to previous results (e.g the email from the first step) might require some further engineering. We will explore what this might look like soon.

Switch statement

If we want more than a binary choice we can use a switch statement in combination with a variable to keep track of state. Once a certain event (like a click on a button) has occurred, the inner component changes the state to allow for the next component to be rendered. We can see an example of this in listing 9. We can see the switch statement (called cond in ClojureScript) in lines 7-10. The current step and last returned value are saved in local state (lines 2-3) and updated in the last line.
 (def register
   (core/local-state
    {:step 0 :last nil}
    (core/handle-action
     (core/dynamic
      (fn [[_ inner]]
	 (cond (:step inner)
	       0 (login-information)
	       1 (verification-code (:last inner))
	       2 (personal-information))))
     (fn [[outer inner] ac]
	(c/return :state [outer (assoc inner :step (inc(:step inner)) :last ac)])))))

A neat feature of using a switch is that we can also work non-linear by using something like a keyword instead of a number, as well as adding another switch. Listing 10 shows the same example of listing 9 but with the ability to move non-linear. We could even visit some steps multiple times or loop infinitely. This however has the downside of requiring two switch statements, meaning any change needs to be implemented and tested in two places.

 (def register
   (core/local-state
    {:step :login :last nil}
    (core/handle-action
     (core/dynamic
      (fn [[_ inner]]
	 (cond (:step inner)
	       :login (login-information)
	       :verification (verification-code (:last inner))
	       :personal (personal-information))))
     (fn [[outer inner] ac]
	(c/return
	 :state
	 [outer
	  (assoc inner :step (cond (:step inner)
				   :login :verification
				   :verification :personal
				   :personal :login) :last ac)])))))

In both listing 9 and 10 we already see the advantage of the loose coupling we can achive thanks to actions in reacl-c. An implementation in React would require callbacks to handle the communication between steps and the parent component that holds them. This not only could lead to bugs, as the callback is incorrectly setup or handled, but also means every single step needs to be build to accomidate a callback.

The intuitive and straightforward way we can work with switch statements make it a popular choice for sequences like this. While reacl-c removes a lot of the edge cases by eliminating callbacks entirely, this structure still suffers from some shortcomings. While possible to make this composable (e.g using callbacks or actions), it’s neither immediately obvious how nor without issues. It is once again worth mentioning, to make this composible would be much more work in React, as we have the advantage of actions here.

To make it composable we could add behavior that emits the last result upwards one more time (in the last line of listings 9, 10), so that we can catch it in a future switch structure, that could be wrapped around this (e.g register in listings 9, 10). While this would lead to the same spread out code of the previously mentioned nested if structures, it does have the desired effect of making it composible, in the sense that we can add things infinitely. A big issue is the amount of boilerplate needed to produce such a structure. Having to write +16 lines of code for three simple steps will add up. A long sequence of 9 steps composed by 3 x 3 compositions would be more than 48 lines of code, for logic that defines an order of 9 steps. The overall impression is that with a switch structure simple things are easily done, but complex structures lead to problems.

Callbacks

Earlier we mentioned that the structure using an if statement could be enhanced to be composible, by using callbacks. Listing 11 shows the previously in listing 8 defined login-info-and-verification-code component now enhanced for an arbitrary next component. It goes further by constructing the entire sequence (lines 13-14) shown in figure 2 and implemented in listing 9 & listing 10.
 (defn-item login-info-with-cb [callback]
   (core/local-state
    {:email nil}
    (core/handle-action
     (core/dynamic
      (fn [[_ inner]]
	 (if (nil? (:email inner))
	   (login-information)
	   (callback (:email inner)))))
     (fn [[outer inner] ac]
	(c/return :state [outer (assoc inner :email (:email ac))])))))

 (def register
   (login-info-with-cb (verification-code-with-cb personal-information)))

Once the first step has emitted something we set the inner state to its result, which causes our structure to call the provided callback that contains our next step. Again, this benefits massively from the ability to use actions, as the callback now only has the job of returning the next step, instead of indicating the next step, moving state and containing the next step.

This is an elegant solution to compose sequentially. However, the amount of boilerplate is still a lot, considering that each composition needs +8 lines of code (original component not included). But we run into the same issues as with our switch structure. While there are solutions for this, the problem is more about the fact that we need to implement this very fundamental building block of sequential composition ourselves and that we need to implement it every time, while taking care of each edge case. Obviously this isn’t the case for parallel composition, which is the reason for why we love to work with it so much. We’d like to draw attention to this fact. Creating these sequences isn’t difficult because of its domain, but rather because of the (lack of) support.

Conclusion

We’ve now looked at three different approaches to building sequences. Current structures either don’t offer composability or they require a lot of boilerplate to be composable. It is possible to write functions which abstract that boilerplate code away, but that takes both time and effort. Support from modern frameworks could not only save developers time but also deliver smooth integration with other features of the framework. But how would that integration work? In the next chapter we will explore how composing sequentially in reacl-c might look like from the developers point of view.

How we might want to build sequences

In the previous chapter we have seen how current tools to compose sequentially either exposed us to a lot of edge cases, had cumbersome problems or forced lots of boilerplate onto us. Let’s take that knowledge and use it to design a more pleasant developer experience, without compromising on how powerful our tool to compose sequentially, would be. But what does that mean? What are some features that our implementation would need to provide?

What it should do

Our sequential composition needs to handle the synchronicity of the domain. That means only one value is to be shown to the user at a time. It’s often the case that a step depends on information provided in the previous step. As such it is important for our composition to allow future steps to access the values returned by earlier steps. Because of similar requirements with synchronicity and order, it should also be possible to compose other asynchronous operations such as HTTP requests. Lastly, individual steps should be fully composable. They should be shareable and have the ability to nest in complex ways. While these are solid requirements we also need to keep in mind that our sequential composition needs to be intuitive and easy to use.

How a developer should be able to use it

Unlike parallel composition (where we can look to reacl-c and React for references) there are few examples to go off of when it comes to our sequential composition. Because of this it makes sense to draw inspiration from other, already well established, functions. ClojureScript provides something that allows us to define things in a neat way with the let function cite:&clojure-let-code. As seen in listing 12, a let is composed of two parameters. The first parameter is a list of key:value pairs, where a key is nothing but a name for a symbol that will be used within the let and value is the actual value of that key. The second parameter, also called body, is a function which has access to the previously defined keys. It will be run once all the values have been computed. We will take a closer look at how let achieves this, towards the end of this thesis. #+CAPTION[Using let to bind values to the names one, two, three.]: Using let to bind values to the names one, two, three. The body is the last line, which is an operation that returns 6.
 (let [one 1
	two (+ 1 one)
	three (inc (* two one))]
   (+ one two three))

Adopting this style for sequential composition has many benefits. The most obvious is that ClojureScript developers would already be familiar with it. Even reacl-c offers a let style macro in the form of ref-let cite:&reacl-ref-let. It also satisfies a lot of our requirements, like being able to access prior results and only executing one pair at a time. Listing 13 shows how our earlier register example could look like if we used a let like style. We call runner and pass our key:value pairs for each step. Where value is the actual step and key is a name which the returned value will be associated with. Note how little code is needed. This example still closely follows let in that it needs an uneven amount of arguments, in which the last is a function that will be executed (with access to all the previously declared keys) at the end. #+CAPTION[Showcase earlier registration process in a let style.]: Earlier register example written in a let style. personal, verification are now names for the returned values of their respective steps, that can be used to access these values further down.

(runner [personal (personal-info)
	   verification (verification-code personal)]
	  (personal-information))

A possible alternative to this would be to omit the body function entirely and instead use the element previously placed in the body (personal-information in our example), as the last element in our key:value pairs list. See listing 14 for an example. While not commonly used like this, let also allows for this style. This has the benefit of creating a concise and consistent look and feel.

(runner [personal (personal-info)
	   verification (verification-code personal)
	   info (personal-information)])

The runner function executes our composition and should be able to be used just like a regular Item when wanted. It should be further composibly in parallel with other Items and actions should be able to be caught from it.

Our runner looks like a shortened switch structure. To enable composition, we will make use of monads. Each step on the inside will be able to be further composed, by using the bind function (to compose multiple steps together) and return (to create a composible type). Developers can use these functions to build up their sequential compositions outside of the runner. The runner itself will then run these compositions, as well as the compositions provided via the key:value pairs. Listing 15 shows another way for us to express our sequential composition, using the bind function. Developers will be able to mix the two styles interchangeably.

(runner
 [_ (bind
     (personal-info)
     (fn [personal]
	 (bind
	  (verficiation-code (:email personal))
	  (fn [verification] (personal-info)))))])

This is just a first glimpse of what it will be like to work with our implementation. In the coming chapter we will further explore how we can compose and how what role the runner plays in our composition.

API Design

This being the introduction of sequential composition into reacl-c, it was important to provide strong primitives. Reacl-c already has excellent tools for parallel composition, so the sequential composition should be closely aligned with them, to be intuitive for developers. The implementation needs to also hide the heavy lifting done in the background and not cause any unexpected issues that would cause it to become unusable for challenging scenarios (e.g long sequences). The sequential composition should not interfere with the parallel composition and the borders between the two should be clearly visible. Most importantly though is that sequential composition should be easy. In this chapter we will introduce our API from a higher level by talking about types and available functions, as well as sharing thoughts on behavior in edge cases.

Types of our API

While ClojureScript is a dynamically typed language, it is helpful to create types using Clojure’s records to make handling and transforming data easier. The most fundamental type is an UI element, which reacl-c already supplies in the form of Item. To signal that the next step should be executed the Item needs to emit something which can be recognized internally. For that purpose the Commit record exists. If an Item emits a Commit, the internals will execute the next step. A developer could just pass an Item for composition, however, it makes sense to have the developer acknowledge that they’re working with more than just a simple Item. After all, the Item should at some point emit a Commit to change the currently shown step. So, to be able to use the Item for sequential composition, the developer needs to wrap it in a Prog. This signals that the developer understood that the Item will eventually emit a Commit. In short:

  • Item: UI element
  • Commit: What an Item emits to signal that the sequence can continue
  • Prog: An Item that will emit a Commit

Internally Prog has a subtype called Bind, which is the result of a then call (see next chapter). The Bind holds both a Prog and a continuation. The continuation will be called once a Commit has been captured from the Item inside of the Prog. Because it is a subtype, every Bind is also a Prog. This will enable us to endlessly compose Progs with bind.

Functions exposed by our API

To deliver on the promises of frictionless composability without loss of performance, monads are used. Because of that, the API needs to provide the return and bind (here called then) functions to be considered a monad. Further, to display a Prog or Bind easily, a show function has been added. The most important function is runner, which executes a Prog or Bind inside of it, allowing it to walk through the provided steps. Table 3 shows an overview of the signatures of our primitive functions.

NameSignature
returna -> Prog a
thenProg a -> (a -> Prog b) -> Prog b
runnerProg a -> Item
showProg a -> Item
make-commita -> Commit a

We will also provide enhanced versions of then and runner, that feature the let like structure that we saw previously. Table 4 shows their signature. Forthcoming We will mark primitive versions of then and runner by appending a -, so -then & -runner.

NameSignature
runner[(a, Item)] -> Item
then[(a, Item)] -> Prog b

We will go into detail about these enhanced versions during the implementation chapter. For now we’ll focus on what we call our primitives (non-enhanced).

return

The return function takes an Item and turns it into a Prog. This allows it to go from a parallel composition (with an Item) to a sequential composition (of a Prog). Once an Item is a Prog the result can’t be further parallely composed.

then

The then function is what allows us to compose multiple Progs together. For that it takes both a Prog and a continuation function (which should return another Prog). then actually creates a Bind (subtype of Prog). The continuation will be called later, in the runner function. The goal of then is to allow for easy composition, just like div from the dom namespace of reacl-c. Further composing of a Prog into another Prog can be done again with the then function. It is important that the order of execution will be preserved, no matter the depth of composition.

runner

A Bind cannot be placed directly into a reacl-c Item. To do so, either show or runner need to be used to translate the sequential composition back into a parallel composition. While show just displays the Item inside, the runner function acts as a window into the sequential execution, as it captures emitted commits and cycles through the given steps. It takes a single Bind (or Prog) as an argument, which could contain further Binds inside of it. Once a commit is emitted from the Bind that it displays, it calls the continuation of the Bind and displays the result of that continuation. If the result is another Bind, emitting another commit will trigger a call to the continuation of the new Bind, which should produce yet another Bind etc.

show

show extracts the Item from the passed parameter, allowing it to be displayed. If it’s a Prog it just takes the Item inside of the Prog and displays it. If it is a Bind, it first takes the Prog inside, then shows the Item. If an Item is passed, the same Item will be returned. Show serves as one of two ways to turn a sequential composition back into a parallel one. This however does not capture any emitted commits. If the execution of sequential composition is desired, runner should be used instead.

Building sequences with our API

Having now seen our API we’d like to show how we can build the previously in figure 2 described sequence, by using our API. For that, we’re going to use our enhanced versions of runner and then first. Listing 16 shows the relevant code.

(def register-steps
  (then [personal (login-information)
	   verification (verification-code (:email personal))
	   info (personal-information)]))

Thanks to ClojureScript’s extensive feature-set we can make our API look identical to what we envisioned in listing 13.

A similar version of listing 16 can be seen in listing 17, where we use our primitive -then instead. Notice that we also need to wrap our components (line 3, 5, 7) in return, to turn them into Progs. #+CAPTION[Using primitives provided by our API to implement the registration process.]: Using primitives provided by our API to implement the register sequences from figure 2.

(def register-steps
  (-then
   (return (login-information))
   (fn [personal]
     (-then (return (verification-code (:email personal)))
	      (fn [verification]
		(return (personal-information)))))))

Primitives and enhanced functions can be used interchangeably. We will see how during the examples chapter, towards the end of this thesis.

The (parallely composed) components that we use need a slight change to work with our API. If they want to trigger the next step, they cannot just emit any value, but rather need to emit a Commit that contains that value. A Commit can be built by using the make-commit constructor.

Obviously it is possible to take our register-steps (which now is a Prog) and further compose it. Listing 18 shows how we can add another step. This again works interchangeably with enhanced and primitives.

#+CAPTION[Further composing register-steps.]: Adding another step to the previously in listing 17 defined register-steps. Assuming show-legal-notice is an already defined component.

(def register-steps-with-legal
  (-then (register-steps)
	   (fn [] (return (show-legal-notice)))))

Our sequential compositions can be executed by putting the resulting Prog into a runner (e.g. (runner register-steps)). The result of that will be an Item that reacl-c can work with. Emits that aren’t a Commit will pass through the runner and be available to the parent component.

We’re pleased to see that our sequential composition is possible just how we envisioned it. Next we’re going to talk about some design decisions, before showcasing our implementation.

Design descision

By using let as an inspiration and choosing monads we’ve already inherited a lot of good decisions and examples for how our API should work. Due to the nature of living nearby parallel composition it is important to define how our API should behave during certain scenarios. First, we’re going to look at how parallel and sequential will interact. Afterwards we will talk about what an “end” for a sequential composition means.

How parallel and sequential composition interact

The developer should be able to use the API like they use the other tools of reacl-c. At the same time though, there needs to be a clear border between the parallel and sequential composition, as they’re fundamentally different. To guarantee that, the API introduced the Prog and Bind types. While neither works with the other reacl-c tooling (to discourage incorrect usage), both contain an Item. Taking a Prog (or Bind) and turning it into an Item is simple, thanks to runner and show. Turning an Item into a Prog is also simple and can be done with return, Functionality that could check if an Item will ever emit a commit (or other types), would be something to add in the future. Perhaps an additional keyword like :state: for the return function of the core namespace in reacl-c could be added to handle this case. It’s worth mentioning that at the borders further composition of the type that has been moved away from, isn’t possible anymore. For example: A runner returns an Item which from that point on can only be meaningfully parallely composed. Likewise, wrapping a Prog within a div with other Items is also meaningless. The developer needs to make a choice at those points if they really are done composing, in order to switch to the different type.

What is the result of the last continutation?

There are multiple options for what this behavior could look like. The most obvious answer to the question of what a runner will return at the end, is that it will show the last Prog indefinitely. It could also stop displaying anything, though there is little benefit to that. A more interesting implementation would be to let the developer return whatever they like in the last continuation of the last Bind. So instead of unwrapping a Prog into an Item to use with other reacl-c functions, the runner could return a normal value at the end. This has the benefit of making our runner be more than just a display, which will turn into a dead end. A possible use-case would be the chaining together of HTTP requests where only the result is important to the application. However usability would suffer, as the developer would need to check if the received value from a runner is an Item, which should be displayed, or a value, which is to be used for further transformative purposes. An extension of this idea would be to allow the developer to pass in a body as the last parameter, much like when let is used. If a body function is provided, the function is given access to all of the intermediate results of the Progs in the runner and the result of the body function is returned. If no body is provided, no result will be returned, the last Prog will just be displayed indefinitely. Like with the previous implementation, this would also suffer from needing to pattern-match the returned value.

In many frontend frameworks these options would be all that is possible, but because reacl-c allows us to emit actions which propagate up the item tree, we can do more than to just display the result on the screen or have the data be returned from the runner in its raw form. Thanks to this, the result of the last continuation could be emitted as an action and be caught by a handle-action function which wraps the runner. This is not perfect either however. One might think that this would mean the pattern-matching might be optional, but it is not. In reacl-c an action must be caught by something. If it is not and the action reaches the top level item, an error is thrown. By allowing the result to be emitted it is possible to accidentally send an action upwards, by returning something in the last continuation from within a runner. This would result in every single runner needing to be wrapped by an additional handle-action. One could argue that using handle-action to catch the returned value, instead of using a function around the runner, like cond, is more idiomatic, as the developers are already using handle-action to catch actions in the entire reacl-c app. This implementation again could be extended by allowing for the last parameter to be a body function, like with let. If the body function is present, the developer can be sure that the runner needs to be wrapped by a handle-action. If the runner is only made up of Progs, the developer does not need to do anything. This makes it possible to clearly express when something needs to be caught, but is open for improvement as it requires additional knowledge about how the runner works. But what would that body function look like? It seems more intuitive to just react to the result in the body function, instead of additionally wrapping the runner with a handle-action. This implementation also has the problem of not being able to warn the developer that they didn’t wrap their runner with a handle-action. It does make sense to provide a handle-runner function which combines this functionality, by taking a Bind and a function that will handle actions. Actions inside of the runner won’t be returned, but emitted. However, this might be too close to the other implementation and, as an additional function, cause confusion.

The mentioned options all come with downsides. It is important to look at the use-case of the runner, to determine which is suited best for use. The most obvious use-case is regulating the flow of an entire app. From login, to a dashboard and further. Here what is returned doesn’t really matter, as the individual results of the steps that the sequence produces are more important than its final result. If we look at creating a sequence for a singular workflow, like adding an item to an ecommerce store, the result might be important. It is likely that we want to let the app know that something happened (e.g product added, refresh items), which could also be solved by giving access to the result. Just returning or emitting the last result might be too intrusive (as it forces developers to always wrap runner), but the option to supply a continuation as a “body” could work well here, as it allows the developer to react to the result of the last step. Another use-cases is the conditional loading of data (from a server). Here the result does matter and we need to provide the possibility to react to it. Of-course, the developer could just add another continuation which reacts to it, but that is rather a hack. The ability to supply a continuation as the “body” would be a great fit, too.

Let’s determine the best fit. Seeing how all three of our use-cases benefit from having the option to react to the result, the implementation that just displays the last Prog indefinitely or shows nothing - is of little use. Using the actions of reacl-c is nice, but causes unwanted complications. Giving developers the option to handle the result or ignore it, by passing a continuation as a body, allows for all use-cases to work and causes minimal overhead for the developer. This also mirrors the functionality of let. This however raises the question what should happen if no continuation is supplied. Should the last Prog be shown indefinitely? From a user experience perspective it’s expected that an action has a reaction, thus it makes sense to not show something indefinitely, but rather display nothing. Another benefit would be that sequential composition is cleaning itself up, after being done. The issue of receiving no continuation could be avoided entirely, by always requiring a continuation. The downside to this would be a minor annoyance for developers, but makes sense for internal use, as less code is needed to implement the above behavior. The API will enable developers to not specify a continuation, but it’ll actually pass an empty function instead.

Implementation

Previously we have discussed which functions our API should offer. Now we will focus on implementing these functions and their associated features. Aside from delivering the necessary functionality, we will also talk about optimizing the runner function and implementing macros for ease of use. The entire source code for our implementation can be found here cite:&seq-comp-reacl-c.

return & -then

The -then is arguably the most important function as it needs to compose steps together. However, the idea behind it is trivial to understand. Take a Prog and a continuation and return a Bind, which is nothing but a container record type that holds both of these values. Listing 19 shows the definition of the Bind record type as well as the return function, which is just a constructor for our Bind type.

(defrecord Bind [prog cont])

(defn return [prog cont]
  (->Bind prog cont))

However, just getting a Prog every time would be of little use. A Prog just contains a single step. Things get interesting if we want to pass a Bind, because we cannot just wrap the Bind again, as it already contains a Prog. If that is the case, -then needs to change the order of execution, to prevent undesirable nesting inside of the Bind. We want our Prog part of the Bind to always be shallow for optimization and bookkeeping purposes (see Tail Recursion Optimization). Thanks to the earlier mentioned Law Of Associativity for monads, we can use Continuation Passing Style (CPS) Transformations to swiftly change our previous continuation into something that gets rid of incorrectly nested calls. This is done by taking the Prog from the passed Bind and using it again as our new Prog. The new continuation is an anonymous function which constructs another Bind, by calling the continuation of the passed Bind with what is passed to the anonymous function (to create a Prog) and using the passed continuation as the actual continuation of the second bind. A visual explanation can be seen in figure 4. #+CAPTION[Using CSP-transformation]: Prog 1 is lifted from the given Bind. The new continuation is a Bind out of the previous continuation and the passed continuation.

./images/cps-transformation.png This allows us to avoid having to flatten the Bind anywhere else, which makes showing the Item inside of the Bind trivial. It also guarantees that the order of execution will always be correct, thanks to deconstructing the passed Bind completely.

Now, inside of our -then function we need to handle both cases. For this we differentiate between a -then call where A: a Prog is passed or B: where a Bind is passed. If a Prog is passed, we just wrap the parameters and return a Bind. If however a Bind is passed, we do our CPS-transformation. See listing 20 for the previously described code. #+CAPTION[Definition of the -then function.]: Definition of the -then function. The CPS-transformation can be seen in line 7, as the previous Bind and new Bind are first deconstructed and then reconstructed into a new Bind.

(defn -then [prog cont]
  (if (bind? prog)
    (return (bind-item prog) (fn [x] (then ((bind-continuation prog) x) cont)))
    (return (if (c/item? prog) (make-prog prog) prog) cont)))

To give the developer feedback in case they make an error, we add :pre and :post annotations, which let ClojureScript know to check the types that come into and out of our function. In this case we say that the prog can be a Prog (or its subtype Bind). The cont parameter needs to be a function and the result of our operation should always return a Bind.

-runner

The place for our monad to be executed in is the -runner. It will receive a Prog (or Bind). The -runner is the most complex function in our API because of all the things it needs to do:

  1. Show current step
  2. Bookkeeping of state for steps
  3. Catch emitted Commits
  4. Make sure implicit state is passed to the Prog (without leaking own state)
  5. And optimize function calls to prevent stackoverflow

As such we will show the code in its entirety once in listing 21 and go in depth about individual parts one after another. #+CAPTION[The definition of the runner function.]: The entire definition of the runner function using trampolines, state-management, lenses and actions.

 (defn -runner [p]
   {:pre [(or (bind? p) (prog? p))]}
   (core/local-state
    p
    (core/dynamic
     (fn [[_ inner]]
	(core/handle-action
	 (core/focus
	  first-lens
	  (show inner))
	 (fn [[outer st] ac]
	   (if (and (commit? ac) (bind? st))
	     (core/return :state [outer ((bind-continuation st) (commit-payload ac))])
	     (core/return :action ac))))))))

Basic -runner Functionality

The goal of the -runner is to hold and display what is inside of a Bind (or Prog). While it does this, it wraps the Bind and waits for a Commit which will trigger it to call the continuation of the Bind. To understand this better we’re going to focus on lines 7-14 of listing 21. First notice the core/handle-action call. We pass a core/focus function to focus the state on a specific part. Namely limiting the implicitly passed state to what is outside of the runner, instead of leaking the bookkeeping state of the -runner downwards. This function again takes two parameters. First a lens (function of two arities) and second an Item. We will talk more about the lense in the next chapter. The second parameter is the show function defined in our API, which just takes either Bind, Prog or Item and unwraps it to an Item again. From this point on we can talk about the function that was passed to the handle-action, which takes up lines 11-14. Like mentioned earlier, that function has two parameters. First is the state of the -runner at the moment at which the action was emitted from the Item. We have access to this, so we can reduce the state with the second parameter, the action which the Item sent, into a new state. We then return that new state with the core/return function (using the :state keyword). This lets the component know that it needs to update its state, therefore render itself again. In the parameter definition of our function (line 11) the state is destructured into the outer state, which was implicitly passed to our -runner and the state of the -runner itself (here named st). In line 12 we have a check to confirm the Item sent us an action that is a Commit. If it isn’t a Commit, the action will propagate further upwards because of the core/return call in line 14. We also check if our current state holds a Bind, because only if we have a Bind, can we call a continuation. In line 13 we then call the continuation of our Bind with the payload of the Commit and return it as state of our -runner.

Using lenses to hide state

The previously mentioned lens in line 9 is needed to stop leaking the bookkeeping for our tail call optimization, downwards into our Item. Lenses are a popular mechanism in functional programming to, on one side, restrict the available information, while allowing changes from the restricted side to change the whole, as well. It does this by providing two functions: Yanker and shover. The yanker grants access to parts of the whole. If parts of that whole are changed, the shover is called to marry that part again with the whole, so the update can trickle upwards again. Our lens is a “first lense”, because it restricts access to anything but the first element. This is relevant because when state is passed around in reacl-c, it usually comes in the form of a list where the first element is the outer and the second element is the inner state. The code for the first-lens can be found in listing 22. #+CAPTION[Explaining the first-lens.]: Elements of the list are destructured into first and rest, where on the yanker side rest is discarded and on the shover side first is ignored in favor of small. small represents the previously passed first, which now has been updated by the restricted side.
(defn first-lens
  ([[first & _]]
   first)
  ([[_ & rest] small]
   (vec (cons small rest))))

Because we don’t want the internal state introduced by our local-state call (inside of our -runner) to leak, but we do want the state surrounding the -runner to be passed down, we ignore the inner state (bookkeeping) and pass down the outer state (implicitly passed state). Our lens is a function with multiple arities. That means it can take different amounts of parameters. Ours being of arity 1 and 2, means it has two different signatures. A signature where it gets one parameter and a signature where it takes two parameters. To restrict access, so when it is called from the perspective of the child, the signature with one parameter is called. That is our yanker. Here the passed parameter is destructured and everything besides the first element is ignored. That first element is then returned. On changes to the state from within our child, the signature with two parameters is called. That is our shover. The change being the second parameter, here called small. We again destructure the argument but now ignore the previously named first and instead access the previously ignored rest. All that is left to do is to combine them with cons and return them as a list.

Implementing Tail Call Optimization

Neither Java cite:&schwaighofer-tco nor versions of JavaScript that we use in our browsers cite:&2ality-tail-call, feature Tail Call Optimization (TCO) cite:&unwinding-stylized-recursion. Yet both languages are used as host languages for Clojure (Java for Clojure and JavaScript for ClojureScript). Due to the high amount of nested function calls it is however an important feature for a functional language. With a correct implementation of Tail Call Optimisation it is guaranteed that successive invocations of nested function calls (like our monadic bind then) won’t cause a stack overflow. It can also enable the use of recursion with our bind elements. Something that can enable infinitely repeating workflows. Burdening the developer to worry about depth of composition would be undesirable, as the goal is to create an easy to use API. It is therefore important to add code to our bind logic, to allow us to implement some kind of TCO around it.

While ClojureScript isn’t offering TCO out-of-the-box for every function call, it does provide the loop and recur functions which do a locale rewrite of the code into a loop cite:&learn-clojure-looping. This allows for worry free function invocations, no matter the depth. loop provides a perfectly fine way to get the benefits of TCO for synchronizing functions, but in order to work with the asynchronous, action driven, approach that reacl-c uses, a custom implementation needs to be developed.

For our implementation we can leverage the concept of trampolines. Instead of stepping deeper and deeper into nested function calls, the function is called once and the result, which is a function, is saved. Now for as long as the function returns another function, we will call the result. If a value is returned (that isn’t a function), we will stop and return that value instead. That return will then break our loop. Listing 23 shows a crude example written in JavaScript. See cite:&schwaighofer-tco for a more detailed explanation. #+CAPTION[Example of trampolines in JS.]: Example of trampolines in JS. optimized calls work initially and loops for as long as it returns a function.

function work(y) {
    return (y === 0) ? true : () => work(y-1);
}

function optimized(x) {
    let r = work(x);
    while (typeof r === 'function') r = r();
    return r;
}

Having a basic understanding of trampolines, we can return to our implementation and take a deeper look at how we implemented TCO. Let’s examine listing 21 again in more detail: In our -runner we define the Prog that was passed into the function (named just p) as local state using the core/local-state function (lines 3-4). This is the first part of our trampoline. Next we call core/dynamic, which takes a function that has one parameter. That parameter will be the state of our component, which is why we destructure it in line 6 to outer (which is immediately discarded with _) and inner (which is kept). This is clever, as we now have access to the state of our -runner component, through the parameter of the function. We need to access that state, because we want to both display the Prog that it holds and wait for it to emit a Commit (using the handle-action function). Now, in line 11, we define the function that will be called once an action is emitted. If the action is a Commit, we execute the code in line 13. Here we set our state to the result of the continuation of the Prog of our inner state, by calling core/return with the :state keyword. With that we complete our trampoline. Because we set our state, the component will be rendered again, this time with the updated state, which is the next step in our sequential composition, because it is the result of the continuation of our Prog.

show

To display our sequential composition we can use the runner. If however we just want to display one step (indefinitely), we can use show. This is a simple helper function that is used inside of the runner to display the Bind. At its core it has a cond call, which allows us to react to specific conditions. This is necessary, because if we get an Item, we can just display it. If however we get a Prog, we need to unwrap the Item from it. Furthermore, if we get a Bind, we first need to get the Prog inside of it, before we can unwrap it. Lastly, to make usage easier, if anything else is passed, we display an empty fragment, which is equivalent to nothing. Getting passed neither Prog or Bind is the case after finishing the last Bind. See listing 24 for the entire code of the show function.

(defn show
  [x]
  {:post [(c/item? %)]}
  (cond
    (prog? x) (prog-item x)
    (bind? x) (prog-item (bind-item x))
    (c/item? x) x
    :else (c/fragment)))

Macros

When we discussed our implementation of sequential composition earlier, we used ClojureScript’s let as an inspiration. We choose that function, among other things, because it’s key:value structure is well understood and is much easier to read than nested anonymous function calls. However, to achieve our goal of providing a let like structure we need to make use of a ClojureScript feature called macros. In the following pages we will explain what macros are, talk about why they’re used and show how to write them. At the end we will showcase our macros and give insight into how they work.

What are macros?

Macros are a powerful feature which lets us rewrite our ClojureScript code before it is being evaluated. That allows us to use all of ClojureScript’s functions to manipulate the input code. This is made possible partly because ClojureScript is a Lisp, so the code already looks like a Clojure data structure. The language uses this to its advantage to operate on itself. The return value of a macro will be a list of code, that will then be evaluated. Because of this we can use the entire language to transform our code, like we transform data, into something more usable. Macros can be found all over Clojure and ClojureScript. Functions like when cite:&clojure-when and when-not cite:&clojure-when-not are actually macros that rewrite themselves to a simple if. Even the already much discussed let function is a macro cite:&clojure-let-code.

What are the uses cases for macros?

Macros provide so much freedom that they enable us to enhance not just our API but also the language itself. If the problem is beyond manipulating data, but rather about manipulating code, macros are a good fit. They allow a developer to provide their API exactly how they imagine it. That’s why we will make use of them in our implementation. However, while things like binding symbols to values under the hood can be used to make things easier for the developer, it can also cause confusion as developers have no idea where the symbol actually came from and can only assume that it works because a macro is used. That assumption gives them little information though. They can also lead to confusing error messages, as another step is added before the evaluation. And because of their freedom they can be complicated to implement. Lastly, as we’re going to discuss later, macros aren’t as easy to implement in ClojureScript as they are in Clojure. So macros must be used with care, even if they can be a tremendous help.

Why are macros used here?

We mentioned earlier that we need to use macros to get our let like structure for our runner. But why is that? Listing 17 shows the previously shown (figure 2) register process, built up with our primitives (non-macro) then.
(-then
 (login-information)
 (fn [personal]
   (-then (verification-code (:email personal))
	    (fn [_]
	      (-then (personal-information)
		     (fn []))))))

While this already enables sequential composition it is still far off from the easy to read, concise API we had envisioned. The developer needs to create the anonymous functions by hand, everytime. Thankfully, the functionality of binding keys to values stays the same, as results of the components, like login-information, will be bound to the parameter in the continuation (e.g the symbol personal). Because this is functionally identical we can write a macro to translate the code in listing 25 into the desired let like structure (seen in listings 13 & 14). Before we dive into the macros themselves, we first need to take a look at how macros in ClojureScript work. This includes both the syntax and the necessary setup to generate macros.

Macro Syntax

Macros allow us to delay evaluation of just some parts of our code. That is a powerful tool. ClojureScript provides a couple of new symbols so developers can describe how it should evaluate code. We can use these symbols inside of a defmacro function, which works similar to defn and def with which we define functions and values with. Table 5 shows a list of some of the available symbols.
SymbolNameFunction
'QuotingStop execution
`Syntax quotingLike ' but qualify with namespace
~UnquotingStart execution

We can quote our code with ' or ` to tell Clojure(Script) not to evaluate it cite:&quick-clojure-macros. Quoting can be thought of as if we wrap our expression in literal quotes. Here is an example of quoting with JavaScript code: [1,2,3].sort() and "[1,2,3].sort()", where the last example isn’t an expression anymore, but a String. In Clojure(Script) we can turn our quoted code back into an expression by using the ~~~ symbol cite:&quick-clojure-macros. The difference between the ' and ` symbol is that ` qualifies each expression with their full namespace. So `map gets turned into cljs.core/map instead of just map. Syntax quoting is an important tool for writing macros as it allows us to to control how symbols are interpreted. We will see later how this is used to create macros that need to treat some symbols in a special way.

Writing macros for ClojureScript

Both Clojure and ClojureScript have access to macros, though implementing one is more complex in the later. This is because macros are always expanded by Clojure, even if they may produce ClojureScript code. This means Clojure is always involved, even in pure ClojureScript projects. So the compilation process needs to be kept in mind when writing ClojureScript macros. You cannot, for instance, put macros in a .cljs file. Regardless, it is still possible to both write macros for ClojureScript and write macros that use ClojureScript code. There are multiple ways to write a macro for use in ClojureScript. The easiest way is to write all of our code in a .cljc file, instead of a .cljs file. In such .cljc Clojure and ClojureScript code can be placed next to each other. It is even possible to tell the compiler which code to use for Clojure and which to use for ClojureScript, using reader conditionals. Just renaming our file isn’t enough though. Because we’re not inside of Clojure, we cannot directly require our reacl-c code. ClojureScript gets around this by allowing developers to reference functions by specifying their entire namespace. So if we usually require reacl-c/core and alias it to just core (so we can call e.g core/dynamic) we now need to specify the entire namespace everytime like this: reacl-c.core/dynamic (for a dynamic call). That is a small price to pay though as macros give us an incredible amount of freedom.

then macro

The goal of our macro is to rewrite the passing of multiple Progs into a series of then calls, which then bind the result of each step to a symbol. Like with Clojure’s let, we want to pass a list of key:value pairs to our macro. Internally the macro will change this to correct ClojureScript code. Correct, because just putting undefined symbols into a list like [this or that 1] wouldn’t work. Listing 26 hints at what needs to be done. #+CAPTION[Showcasing what the then macro does.]: The macro will take care of wrapping Progs in a -then and creates an anonymous functions each time (while binding keys to function parameters)
;; Before macro
(then [x prog1
	 y prog2
	 _ prog3])

;; After macro
(-then prog1
	 (fn [x]
	   (-then prog2
		  (fn [y] prog3))))

The strategy will be to generate the anonymous functions and to use the supplied keys (in our case x and y) as the parameters of our newly generated anonymous functions. This is possible because we need to supply a continuation to our then function anyways. That continuation can have any amount of parameters, but for this to work we only need to give one. Listing 27 shows the macro in its entirety. The core idea of the implementation is heavily influenced by Lei Ray’s video on “Monads in Clojure” cite:&lei-ray-monads-clojure.

#+CAPTION[Entire code for the then macro.]: The entire macro that wraps our Progs with then and generates the anonymous functions.

(defmacro then
  [[var val & rest :as steps] end-expr]
  {:pre [(even? (count steps))]}
  (if steps
    `(code.bind/-then ~val (fn [~var] (then ~rest ~(seq end-expr))))
    end-expr))

Our macro takes two arguments. A list of values and an end expression, just like let does. The list of values is then destructured into three parts. var, val and rest. The var will be our symbol, val the value our var will be associated too and rest is what is left of our list. After making sure that our list is balanced, meaning it has just as many vars as vals, we check if we have steps left. Steps is just a reference to our var and val, as well as rest. If that is the case, we stop execution of our code with the \` symbol and begin to build the macro part of our function. We want to call the primitive then from our bind namespace, like mentioned earlier, to do this we need to spell out the entire namespace, as we’re currently in a .clj file and cannot import the .cljs namespace. Our primitive then takes two parameters. First a Prog and second a continuation function. We use the \~ symbol to undo the syntax quote and pass the val as is. Then we continue and build our anonymous function. For our macro to work just like let we need to pass our var, which stands for our symbol (e.g. x and y ), as the parameter of our anonymous function. We again undo the syntax quoting using \~, so our actual value is being placed. Inside of our function we then do a recursive call to our macro, once again undoing the syntax quoting to pass the rest value of our list and our end-expr (which is wrapped by a list, to stop it from being executed by ClojureScript). Our end-expr will be executed once we’ve worked through all steps. Important to remember is that end-expr itself might try to access the symbols given in to our then. This now works, because end-expr is at the bottom of all of our anonymous functions, which provide the context in which these symbols are bound to values. That is because we have actually not executed the code, but transformed it by using syntax quoting. See listing 28 to see how the nesting of anonymous functions works out, to allow end-expr to have access to the symbols at the end.

 (then prog1
	(fn [x]
	  (then prog2
		(fn [y]
		  (then prog3
			;; assuming end-expr
			;; accesses x y z symbols
			(fn [z] (end-expr x y z)))))))

Now our then macro can be called exactly like we would with let, simply by doing the following: (then [a prog1 b prog2] (fn [] (+ a b))) we are able to chain together prog1 and prog2. What we get back is a Bind of both Progs.

runner macro

With our then we have greatly improved the desired developer experience, however, the result still returns a Bind. That is fine, as we might want to further compose this. However, we still need to wrap our then expression with a runner to run it. To further simplify the experience we will create another macro, this time for the runner function to give developers the option to do everything within a single call. For this we will simply wrap our then macro with our primitive runner from the bind namespace. Again we syntax quote our call and undo the quote for our values. To add even more convenience our runner macro has an arity of two. If the developer is not interested in supplying an end-expr function, we will pass an empty function into the then for them. The resulting code is simple but works exactly as we want it to and can be seen in listing 29.
(defmacro runner
  ([x]
   `(runner ~x (fn [])))
  ([x y]
   `(code.bind/runner (then ~x  ~y))))

Limitations

While the current implementation achieves what it set out to do, some compromises had to be made. First, as mentioned earlier, it is being relied on the fact that the developer actually emits a Commit in what they label a Prog. There is currently no logic to make sure that the developer is forced too or reminded if they aren’t. Another limitation is that the current API offers only primitives and our two macros, but none of the deep functionality which is found in reacl-c for the parallel composability. So functions that map, filter etc. over sequential compositions are not included. There is also no error handling for sequential composition. Developers need to handle errors by hand in the continuation of the next Bind, as there is no Error sub-type of Commit. Lastly, there is no way to terminate early, like with a Maybe monad.

Examples

With our API now defined and implemented we can take a look at some examples.

Register sequence

Our initial example, to explain what sequential composition is, was a register component. That makes sense because modern register processes are often split up into multiple parts. Lets build that three step register process again, this time with our API. Listing 30 shows the sequential composition, which looks both similar to what we previously saw in listing 16, when examining the API and to listing 14, where we thought up this style of sequential composition. It also looks identical to a let call.

(runner [personal (personal-info)
	   code (verification-code (:email personal))
	   _ (personal-information)])

The only necessary change inside of our components is to emit a Commit. See listing 31 for this, where we change the behavior of the personal-info component by returning a commit in line 8, instead of just a value. #+CAPTION[Excerpt of the personal-info component.]: Excerpt of the personal-info component. Parts have been removed for clarity’s sake. This is a parallel composition that emits a Commit once the user presses the button.

 (core/defn-item personal-info
   (core/local-state
    {:name "" :email ""}
    (core/dynamic
     (fn [[outer inner]]
	;; input field code ...
	(dom/button
	 {:onclick (fn [state action] (core/return :action (make-commit inner)))}
	 "Continue")))))

This is all that is needed to create our sign-up process. The runner can now be placed into a parallel composition.

Endless loop

Because our then takes a continuation for its next step, it’s trivial to create an infinite loop. Our macros make this even easier, by abstracting boilerplate code away from us. And because our runner implemented TCO, we can be sure that our stack won’t blow, no matter how many times we have looped. Listing 32 shows an example of an infinite loop using our macros.

(defn infinite-loop [n]
  (then [a (item n) ;; helper function which creates Prog
	   _ (infinite-loop (inc a))]))

(def will-never-stop
  (runner (infinite-loop 0)))

It could be possible that there is an even compactor way, however this implementation is small enough for now. While there probably is an even shorter way of achieving an infinite loop, this implementation is small enough for now. The infinite-loop function calls itself recursively inside of our then (line 3). That infinite construct is then placed within a runner (last line). Worth of note is that the recursive call still receives an integer from the previous step. Also, because this loop never ends, the developer can use _ to ignore its result, like they would also do with a function parameter.

Re-using compositions

Using just the then macro (without the runner), we can save a sequential composition to use it again in multiple places. Because the runner does nothing but call then under the hood, which in turn takes Progs, we can further compose inside of our runner. See listing 33 for an example of an order process, in which the selection of the product is defined outside of the runner.

(def burger-selection
  (then [size size-selection
	   condiments condiments-selection
	   extras? extra-selection]
	  (fn [] {:size size :condiments condiments :extras? extras?})))

(def order-process
  (runner [credentials login-user
	     order burger-selection
	     payment payment-options
	     _ (confirm-order [credentials order payment])]))

Note that we lose track of accessing the results of the previous sequential composition, which happened in the first then. We do, however, technically have access to them, but the keyword (e.g size) isn’t shown to the developer, which means they probably forget about it. Therefore we have to use the end-expr of our first then (line 5), to gain access to these values again in the following composition (e.g order in line 9).

Mixing primitives and macros

Because our macros don’t add additional logic, we can mix primitives and macros! In listing 34 we use our primitive then inside of our macro runner (lines 3-5). Note how we can still access the previously defined symbols (e.g a, b) inside of our primitives. #+CAPTION[Showcasing composition by using the primitive then.]: Showcasing composition by using the primitive then to first compose a complex structure, before inserting it into our macro runner.

(def mix-primitives-macros
  (runner [a (item 1)
	     d (-then (item (inc a))
		      (fn [b] (-then (item (inc b))
				     (fn [c] (item (inc c))))))
	     _ (item (+ a b c d))]))

The resulting order is alphabetical, meaning the result of lines 3-5 are saved in d, as the second last operation. Note that it is still possible to access all the symbols created by the primitive -then (e.g a, b, c), further down in the last step (last line).

Repeatable workflow

In the previous example we saw the benefit of using recursion in our runner. Let’s push this further by going beyond a single step. Some software systems are nothing but an endless loop of the same operations. Ordering processes like vending machines come to mind. These are now trivial to create (and enhance) thanks to our sequential composition. Let’s take a disease testing facility as an example. We’re going to model the following steps:

  1. Enter personal information of patient
  2. Select test type and start test
  3. Enter test result
  4. Print, showcase or send result to patient

Our sequential composition for this process can be seen in listing 35.

(def test-patient-steps
  (then [personal-info acquire-personal-info
	   test-info enter-test-info
	   result enter-test-result
	   _ (showcase [personal-info test-info result])
	   _ test-patient-for-disease]))

(def start-testing
  (runner test-patient-steps))

The components aquire-personal-info, enter-test-info, enter-test-result and showcase are all assumed to be defined elsewhere. We use the same trick of recursively calling our sequence at the end of the composition (line 6).

Noteworthy would be the option to pass the collected information further along, which could be used to collect statistics (e.g for positive cases etc.) by reducing over the information with each successful flow.

Possible improvements

While it can be argued that our API does what it set out to do, there are various ideas that have been pushed aside, to spend more time improving the core of the API. During the implementation of the runner the idea for early termination came up. Similar to something like the Maybe monad, developers could throw a different type of Commit to tell the chain to break. Inside of the runner the developer then would have the option to handle the early termination. Whether this could be done by adding a third parameter or by giving a different kind of runner is yet to be determined. However, giving developers the ability to break the chain at any time could lead to problems, as much like with Progs actually firing Commits, we can’t communicate to the developer that this Prog might terminate early. This could be solved by making a dedicated type that only works with a subset of operations, but the amount of work required to properly implement this is unknown and likely high. But, not just a separate type for terminating early would be of interest. Having the ability to communicate that an error has occurred and then being able to handle that (maybe even in different steps of severity) would be also beneficial. A possible scenario would be a lot of time passing since step 1 and step 2 and the user gets logged out. Instead of having to handle the error either outside of the runner or inside of every single step, we could dedicate a space inside of the runner for exactly that. Besides new types, another improvement would be to force a developer to Commit something inside of a Prog. The implementation for this is unclear, however the feature is of interest as that could be a major source of bugs. Lastly, helper functions for sequential composition, like the ones that exist in reacl-c for parallel composition, could be a possible addition. Things like a def-prog function that works like def-item but also wraps the resulting Item inside of a return or the ability to map actions emitted from a component to be Commits so the actual component doesn’t need to be changed, but can still be used inside of a sequential-composition. There are probably many more applications of the deep pool of functions inside of reacl-c, so these are just some that came to mind. Sadly at the time of writing, there still is a bug with the end-expr of the runner (and then). If it has to change state it will cause the sequence to run twice. After lot’s of testing we narrowed down the possible cause to an issue with how state is managed. This could perhaps be an issue with reacl-c or even React. Further research into this bug would be done if more time was available.

Conclusion

Having used our API to build the earlier mentioned examples, it is clear to us that our implementation delivers what it set out to do: Enable sequential composition in a developer friendly way. Creating sequences of steps is now much easier and composable. Despite all of the work happening under the hood, from tail call optimization to handling the asynchronously, the developer can use the API without ever being bothered by either. At the same time usage is simple, thanks to taking an already well understood way of working with data, like let, as an inspiration. While the API could be enhanced, the added complexity of some proposals could also take away from the currently present simplicity. It’s best to first see how the current implementation solves the problems presented by workflows, before making big additions. We’re pleased with the implementation and are excited to find out what we can build with it in the future. The source code for our implementation can be found here cite:&seq-comp-reacl-c.

References

bibliographystyle:ieeetr bibliography:./cites.bib

List of Figures

List of Tables

List of Listings

About

Bachelor Thesis in Applied Computer Science about Composition of UI elements in reacl-c using Monads.

Resources

Stars

Watchers

Forks

Languages