BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Functional UI - a Stream-Based Equational Approach

Functional UI - a Stream-Based Equational Approach

Bookmarks

Key Takeaways

  • User interface applications can be implemented by following functional patterns. Functional UI relies on an explicit functional relation (the reactive function) linking events received by the user interface and the actions the interface application must exert on the interfaced systems.
  • Three strategies coexist and can be used. The stream-based strategy describes the user interface as the computation of the stream of reactions from the streams of events. The stream-based strategy can be more concise and reveal the dependencies involved in the reactions by abstracting over time.
  • Functional UI frees options for the software architect to delay or reverse decisions that are unrelated to the application specifications. Tech leads have a larger pool of developers to choose from. The provided separation of concerns may result in better separation of teams and skills. The same application may be repurposed to another platform, language, or device by writing an adapter and keeping the reactive function untouched. Functional UI may leverage a large ecosystem of libraries, as no frameworks are restricting its compositional abilities.
  • Functional UI, with Elm as its main proponent, has shown its ability to handle large real-life projects (around 100,000 lines of code). 

Functional UI relies on an explicit functional relation linking events received by the user interface and the actions the interface application must exert on the interfaced systems:

(1) (action_n, state_n+1) = f(state_n, event_n) where:

  •     n is the nth event processed by the application,
  •     state_n is the state of the reactive system when the nth event is processed,
  •     action_n is an encoding of the action to be performed on the interfaced system -- can be thought of as a command,
  •     f is called the reactive function.

By analogy with other domains and to emphasize the importance of that equation, it will be referred to going forward as the fundamental equation of reactive programming. That functional equation has been made popular by the Elm front-end framework. In two previous articles, the fundamental equation was used to derive two functional strategies.

The Functional UI introduction explained how the reactive function can be implemented directly in any programming language. Pux in PureScript, Yew/Seed/Draco in Rust, and AppRun/HyperApp in JavaScript all directly leverage the fundamental equation.

The Functional UI introduction article also emphasized how the equation allows developers to express a web application without the need for a heavy or complex UI framework. HyperApp and AppRun and are both minimal frameworks (below 2KB and 5KB gzipped, respectively) with excellent performance. Elm additionally has proven that the functional UI strategy scales nicely to large sites.

A second article on a model-based approach for functional UI split the state expressed in the fundamental equation into two pieces: control states that determine the computation to perform, and the extended state that parameterizes the performed computation. This divide leads to a visual representation of the behavior of the target web application and a modelization based on state machines. Such model-based strategies have been adopted extensively for the prototyping of interfaces for embedded systems in safety-critical industries -- they enable automated test generation and formal reasoning about properties.

In this article, a third functional UI strategy is presented that relies on streams. Rather than by state, the fundamental equation is split this time by actions and lifted into an equation between streams. This results in a system of equations involving streams of events, pieces of state, and actions.

Let's dive into it.

Towards A System of Stream Equations

We denote actions, state, and events to respectively be the stream of actions, state and events for the reactive system under implementation. For instance, actions is the possibly infinite collection of action_n for all n.

The fundamental equation:

(1) (action_n, state_n+1) = f(state_n, event_n)

can be rewritten as:

(2) (actions, next state) = (fmap f) (state, events)

where next is a function that takes a stream s into another stream t such that t_n = s_n+1, and fmap is a functorial map that lifts a function between values into a function between stream of values.

The equation can further be written as a system of two equations, obtained by projecting over abscissa and ordinate:

(3) actions = g (state, events)
(4) next state = h (state, events)

In practice, the stream actions is often a merge of different kinds of actions (like render screen, fetch resource, or poll motion sensor) and we can write a separate equation for each action kind. Also, a given action kind will only depend on a few events among those handled by the application. It will also depend only on a few pieces of states, rather than on the whole state of the application.

By dividing state and actions into any pieces that are relevant to the problem at hand, we end with a longer system of equations involving streams.

Let's take a trivial example. Consider the user interface for a reservation system with two events: the user clicking a search button, and the response from an API call. Assume that the state of the application consists of the parameters of the reservation, e.g. origin and destination, dates of the booking, and a cart to hold purchases. We have events = {search_click, api_call_response}, and state = {origin, destination, dates, cart}, where search_click is for instance a stream of clicks on the search button. The following equations may for instance hold:

call_api            = g1({origin, destination, dates}, search_click)  
retry               = g2(_, api_call_response)
show_payment_screen = g3(cart, api_call_response)

Those equations reflect that our retry command depends on the response of the API request but does not depend on the dates of the booking. Our API request depends on the parameters of the booking (such as destination) but does not depend on the content of the cart. Similarly, we can also write equations for the pieces of state of the application, and substitute (4) with those equations.

The stream equational model thus consists of writing specifications for the user interface in the form of a system of equations linking the commands to perform on the interfaced systems with and only with the relevant events and pieces of state of the application.

Among the functional languages that specify reactive systems by equations between streams, we can quote synchronous dataflow languages such as Lucid Synchrone, Lustre, Signal, or the more recent Ceu that integrates with C and Lua. The Lucid Synchrone manual contains a simple example dealing with simulating the behavior of a coffee machine.

We will now give an example of implementation in JavaScript, starting with the equational description in terms of streams.

Two-player Chess Game

Let's consider (as in previous articles) a two-player chess game. Implementing the chess game means specifying and implementing the reactive function, connecting it to user events, and running the commands computed by the reactive function.

In this design, we will reuse a chess engine that implements the chess game logic (move validity, game end detection, etc.). This chess engine is an interfaced system and will be operated, like any interfaced system, through commands computed by the reactive function. To execute the move command, we use the chess.js chess engine.

We will also reuse a ChessBoard React component to render the chessboard (render command). Two props of that component are of particular interest to us: position, which encodes a chessboard configuration, and squareStyles which handles the highlighting of specific squares of the chessboard.

Additionally, we will create an event source (user clicking on the board) from the onSquareClick prop of the ChessBoard component. That takes care of the parts of the application which perform effects. We can now concentrate on the reactive function.

Before writing the equations for the reactive function, let's write a test run of the reactive function to check the correctness of our implementation. We choose this test to be short while testing some important properties (no moves possible after the game over, invalid moves not possible, etc.).

Here is an example of run (actually this is among the shortest chess games that can be played):

Event Actions
init render chessboard in the initial position
click a3 none (empty square)
click a8 none (black piece)
click g2 highlight g2
click g5 none (invalid move)
click g4 move piece g2-g4, render new position
click e6 highlight e6
click e7 highlight e7
click e4 none (invalid move)
click e5 move piece e7-e5, render new position
click f2 highlight f2
click f4 move piece f2-f4, render new position
click d8 highlight f2
click h4 move piece d8-h4, render new position
click a2 none (game is over)
click h2 none (game is over)
click e6 none (game is over)

The event/actions correspondence can be represented horizontally, rather than vertically, with a marble diagram. Here, M stands for a move command, and R for a render command (highlight commands are render commands):

Events  :  *-a3-a8-g2-g5-g4-e6-e7-e4-e5-f2-f4-d8-h4-a2-h2-e6
Actions :  R-..-..-R -..-MR-R -R -..-MR-R -MR-R -MR-..-..-..

We will call moves the streams of move commands and renderProps the stream of props for the ChessBoard component.

The previous marble diagram of a sample game can thus be expressed instead as:

boardClicks: *-a3-a8-g2-g5-g4  -e6-e7-e4-e5  -f2-f4  -d8-h4  -a2-h2-e6
moves      : .-..-..-..-..-g2g4-..-..-..-e7e5-..-f2f4-..-d8h4-..-..-.. 
renderProps: R-..-..-R -..- R  -R -R -..- R  -R - R  -R - R  -..-..-..

We keep that marble diagram at hand in order to test our modelization and implementation. Note that we do not make explicit the render props in the marble diagram for lack of horizontal space. We do however detail the move to be realized: g2g4 stands for a move from a piece in g2 to the destination square g4.

We are now going to equationally derive moves and renderProps from boardClicks. We will write our equations in pseudocode inspired by Lucid Synchrone. No knowledge of Lucid is necessary to follow the code as the notation used strives to be straightforward:

  • the map stream operator maps pointwise a stream with a function fn, following the syntax map stream(alt_name) fn. Thus, boardClicks is a stream created by user clicks on the chessboard and whose elements are the clicked squares. boardClick(square) will rename the board clicks stream as square in scope of the subsequent mapping function fn.
  • s1 'then' s2 is a third stream whose first element is the first element of s1 and subsequent elements are taken from s2.
  • prev s gives access to the previous value of the stream s.
  • |> is a forward composition operator (x |> f |> g means g f x).

The following marble diagrams illustrate the semantics of then, next, prev, map, and binary operators:

Stream            
x x_0 x_1 x_2 x_3 x_4 x_5
y y_0 y_1 y_2 y_3 y_4 y_5
x `then` y x_0 y_1 y_2 y_3 y_4 y_5
next x x_1 x_2 x_3 x_4 x_5 x_6
prev x x_0 x_1 x_2 x_3 x_4
map x f f(x_0) f(x_1) f(x_2) f(x_3) f(x_4) f(x_5)
x op y x_0 op y_0 x_1 op y_1 x_2 op y_2 x_3 op y_3 x_4 op y_4 x_5 op y_5

As previously mentioned, the reactive function computes two commands, one to perform a chess move in the chess engine, and another to render the chessboard on the screen with the React ChessBoard component. The move and render commands are described with pseudo-code in the next sections.

Move commands

---- Main types

-- one possible position on the chessboard
Square :: ["a1", "a2", ..., "a8", "b1", ..., "h8"]
-- type synonym for Square, with the added implication that there is a piece on that square
Square => PiecePos
- record describing a move with an origin square and target square
Move :: {from :: Square, to :: Square} | ∅
- stream of board clicks
BoardClicks :: Stream<Square>

---- Application logic

-- a move is either a white piece move or a black piece move
moves = whiteMoves || blackMoves

-- Whites move iff: 
-- the game is not over, 
-- it is Whites turn to play, 
-- there is already a selected piece (origin square), 
-- user clicks on a square which does not contain a white piece (target square), 
-- the corresponding move (origin square to target square) is valid
whiteMoves = ∅ `then` map boardClicks(square)
   case ! prev gameOver & 
        prev playerTurn = White & 
        prev selectedPiece & 
        !hasWhitePiece square & 
        map (selectedPiece, square) isValidMove : {from: selectedPiece, to: square}
   case _ : ∅
-- Black moves is the symmetric version of White moves
blackMoves = ∅ `then` map boardClick(square)
  case ...

-- a piece is deselected (∅):
-- + if the game is over
-- + if the game is on, and a user click triggered a valid move
-- 
-- a piece is selected if the game is on, a user click does not trigger a valid move and
-- + when Whites play and a white piece is clicked on
-- + or when Blacks play, and a black piece is clicked on
selectedPiece :: Maybe PiecePos
selectedPiece = ∅ `then` 
  case prev gameOver: ∅
  case moves: ∅
  case prev playerTurn = White & boardClicks(square) in whitePiecesPos : square
  case prev playerTurn = Black & boardClicks(square) in blackPiecesPos : square
  case _: prev selectedPiece 

-- the game is over if a valid move ends the game. 
-- Invalid moves do not change the status of the game
gameOver = false `then` map moves isWinningMove || prev gameOver

-- Whites initially have the turn, then that alternates with Blacks after every move
- In case of an invalid move, the turn remains the same
playerTurn = White `then`
  case whiteMoves: Black
  case blackMoves: White
  case _: prev playerTurn

-- Board state
-- the position of white pieces is set initially per the chess game rules
-- then it changes with every move performed by the players
-- Achtung! white pieces may be affected by a black piece move and vice versa
whitePiecesPos :: Array of PiecePos
whitePiecesPos = whitesStartPos `then`
  -- remove old white piece position and add new one
  case whiteMoves({from, to}): 
    prev whitePiecesPos |> filter (not from) |> concat [to]
  -- remove old black piece position if any - case when a white piece gobbles a black one
  case blackMoves({from, to}):
    prev whitePiecesPos |> filter (not from)
  case _: prev whitePiecesPos

-- blackPiecesPos is deduced from whitePiecesPos by symmetry
blackPiecesPos :: Array of PiecePos
  ...

Render command

-- The render uses the ChessBoard React component. 
-- We specify some look and feel options for the component.
-- We essentially render a board position (`position`) 
-- and possibly one highlighted piece (`squareStyle`).
renderProps = {
  draggable: false,
  width: 320,
  boardStyle: {
    borderRadius: "5px",
    boxShadow: `0 5px 15px rgba(0, 0, 0, 0.5)`
  },
  onSquareClick: eventSourceFactory boardClicks,
  -- only the following two properties vary
  squareStyles,
  position
}

-- the position *prop* is [as defined by the ChessBoard React component](https://chessboardjsx.com/props) we are using
position = "start" `then` 
  case moves(move): getPositionAfterMove move
  case _: prev position

-- the squareStyle prop allows us to manage the style for a highlighted piece
squareStyles = 
  case selectedPiece(square): { [square]: { backgroundColor: "rgba(255, 255, 0, 0.4)" }}
  case _: {}

Implementation in JavaScript

Unfortunately, to my knowledge, there is no JavaScript library that allows us to write stream equations as the ones just enumerated. The issue lies in handling streams as first-class objects and memoizing the past values of streams as they are required by a given program.

An approximation can however be reached by using libraries that implement a reactivity model. Here we distinguish two groups: stream libraries (RxJS, Most.js, Bacon.js, and more) that expose a stream abstraction wrapping around values, and other libraries that remain at the value level (mobx, reactor.js, the upcoming Vue 3 standalone reactivity module, and even Angular zones used by Angular for change detection).

Libraries in the first group allow defining, constructing and composing discrete streams with a set of operators. However, the semantics of such streams only imperfectly replicate the required semantics for the system of equations to hold, for two reasons.

On the one hand, our equations dictate that all streams have one deterministic value for every index n (i.e. every discrete instant), and the order in which the equations are expressed does not matter. That is the so-called synchronous hypothesis (note that synchronous here relates to synchronization rather than immediate execution -- the usual meaning in a JavaScript context). RxJS however suffers from glitches, i.e. the x = y + z equation cannot be implemented due to the reactivity model's implementation. If y and z change on the same instant, observing x at that same instant n, can result in either one of x_n = y_n + z_n (what we want), or x_n = y_n-1 + z_n or x_n = y_n + z_n-1. The latter two values result from the eagerness of the propagation of updates. There are several ways around this, one of which we will use in our RxJS implementation.

On the other hand, the lack of a unified time between streams requires that developers carefully consider the timing of the connection of the network of streams they are considering. It may happen, among other things, that a stream derived from a stream source connects too late to the source and consequently misses some emitted values. Those considerations are absent from our design (system of equations) but are nonetheless an important factor of our implementation. In other words, those considerations reveal the accidental complexity of stream libraries.

The second type of reactive libraries operates at the value level. Generally, atomic reactive objects are defined, from which other reactive objects are derived. To propose a developer-friendly syntax, most of these libraries use ES6 proxies and intercept the . dereferencing operator to track assignment and dependencies between reactive objects. Here also, additional machinery is necessary to define reactive objects that depend on the past values of their dependencies. In fact, the mobx documentation recommends using stream libraries in those cases:

For anything that involves explicitly working with the concept of time, or when you need to reason about the historical values/events of an observable (and not just the latest), RxJS is recommended as it provides more low-level primitives.

S.js is one exception to the category as it does combine an automatic dependency graph with a synchronous execution engine. Angular's zone.js is another one --- it ensures synchronicity by computing reactions once changes have terminated and before new changes occur.

The following implementation will however use RxJS for its popularity. To make sure we have a unified time while also tracking past values of the involved streams, we use the RxJS's scan operator that manages a stateful accumulator that updates every time a source emits. Implementation-wise, we have one single stream that holds all the values used in the system of equations. The individual streams are then derived from the single stream. Having a single stream is what achieves synchronization of the updates. The scan operator is what memoizes past values.

Excerpts of the full implementation include:

... imports... 

// consts
const NO_PIECE_SELECTED = null;
const WHITE_TURN = "w";
const BLACK_TURN = "b";
const NO_MOVE = null;
const whitesStartPos = [...];
const blacksStartPos = [...];
const highlightStyle = { backgroundColor: "rgba(255, 255, 0, 0.4)" };

// Chess engine
const chessEngine = new Chess();

// Creating the event source
const boardClicks = new Subject();
function onSquareClick(square) {
  boardClicks.next(square);
}

const initial = {
  gameOver: false,
  playerTurn: WHITE_TURN,
  selectedPiece: NO_PIECE_SELECTED,
  whitePiecesPos: whitesStartPos,
  blackPiecesPos: blacksStartPos,
  squareStyles: {},
  position: "start",
  whiteMove: NO_MOVE,
  blackMove: NO_MOVE,
  chessEngine
};
const streams$ = boardClicks.pipe(
  scan((acc, clickedSquare) => {
    const {
      gameOver,
      playerTurn,
      selectedPiece,
      whitePiecesPos: wPP,
      blackPiecesPos: bPP,
      squareStyles,
      position,
      whiteMove,
      blackMove,
      chessEngine
    } = acc;

    let nextGameOver, nextPlayerTurn, nextSelectedPiece, nextWhitePiecesPos;
    let nextBlackPiecesPos, nextSquareStyles, nextPosition;
    let nextWhiteMove, nextBlackMove;

    nextWhiteMove =
      !gameOver &&
      playerTurn === WHITE_TURN &&
      selectedPiece !== NO_PIECE_SELECTED &&
      wPP.indexOf(clickedSquare) === -1 &&
      isValidMove(selectedPiece, clickedSquare, chessEngine)
        ? { from: selectedPiece, to: clickedSquare }
        : NO_MOVE;

    nextBlackMove = ...

    const move =
      nextBlackMove !== NO_MOVE
        ? nextBlackMove
        : nextWhiteMove !== NO_MOVE
          ? nextWhiteMove
          : NO_MOVE;
    if (gameOver) nextSelectedPiece = NO_PIECE_SELECTED;
    else if (move !== NO_MOVE) nextSelectedPiece = NO_PIECE_SELECTED;
    else if (playerTurn === WHITE_TURN && wPP.indexOf(clickedSquare) > -1)
      nextSelectedPiece = clickedSquare;
    else if (playerTurn === BLACK_TURN && bPP.indexOf(clickedSquare) > -1)
      nextSelectedPiece = clickedSquare;
    else nextSelectedPiece = selectedPiece;

    ...

    const nextAcc = {
      gameOver: nextGameOver,
      playerTurn: nextPlayerTurn,
      selectedPiece: nextSelectedPiece,
      whitePiecesPos: nextWhitePiecesPos,
      blackPiecesPos: nextBlackPiecesPos,
      squareStyles: nextSquareStyles,
      position: nextPosition,
      whiteMove: nextWhiteMove,
      blackMove: nextBlackMove,
      chessEngine
    };

    return nextAcc;
  }, initial),
  share(),
  startWith(initial)
);

// Individual streams
// useful for testing separately the intermediate streams
const gameOver$ = streams$.pipe(map(x => x.gameOver));
const playerTurn$ = streams$.pipe(map(x => x.playerTurn));
const selectedPiece$ = streams$.pipe(map(x => x.selectedPiece));
const whitePiecesPos$ = streams$.pipe(map(x => x.whitePiecesPos));
const blackPiecesPos$ = streams$.pipe(map(x => x.blackPiecesPos));
const squareStyle$ = streams$.pipe(map(x => x.squareStyle));
const position$ = streams$.pipe(map(x => x.position));
const whiteMoves$ = streams$.pipe(map(x => x.whiteMove));
const blackMoves$ = streams$.pipe(map(x => x.blackMove));

// Compute the parameters for the commands
const moves$ = merge(whiteMoves$, blackMoves$).pipe(filter(x => x !== NO_MOVE));
const renderProps$ = streams$.pipe(
  map(streams => {
    const { squareStyles, position } = streams;

    return {
      draggable: false,
      width: 320,
      boardStyle: {
        borderRadius: "5px",
        boxShadow: `0 5px 15px rgba(0, 0, 0, 0.5)`
      },
      onSquareClick,
      // only the following two properties vary
      squareStyles,
      position
    };
  })
);

// Execute the move commands
moves$.subscribe(move => {
  const { from, to } = move;
  const moveResult = chessEngine.move({ from, to, promotion: "q" });
});

// Running render commands
renderProps$.subscribe(props => {
  reactDOM.render(<Chessboard {...props} />, document.getElementById("root"));
});

Testing

We already mentioned a key advantage of functional UI, which is the direct mapping to user scenarios. The latter allows to check for correctness, and detect both design and implementation bugs early. As importantly, functional UI also benefits from an easy testing story. Let's go back again to our fundamental equation:

For all n: (action_n, state_n+1) = f(state_n, event_n)

This means:

(action_0, state_1) = f(state_0, event_0)
(action_1, state_2) = f(state_1, event_1)
(action_2, state_3) = f(state_2, event_2)
...
(action_n, state_n+1) = f(state_n, event_n)

If we define h as the function which maps a sequence of events to the corresponding sequence of actions:

h([event_0]) = [action_0]
h([event_0, event_1]) = [action_0, action_1]
h([event_0, event_1, event_2]) = [action_0, action_1, action_2]
h([event_0, event_1, event_2, ..., event_n]) = [action_0, action_1, action_2, ..., action_n]

then h is a pure function! That means we can easily test it, simply by feeding it inputs and checking that the expected outputs are produced.

To illustrate that point, let's consider again our previous two-player chess game. For the sake of this article, we are going to test only the gameOver$ stream whose value switches and sticks to true once the game is over. Our test could simply be written like this:

// Test scenario
// Shortest chess game
const shortestChessGame = [
  "a3", "a8", "g2", "g5", "g4", 
  "e6", "e7", "e4", "e5", 
  "f2", "f4", 
  "d8", "h4", 
  // After move h4, the game is over!
  "a2", "h2", "e6"
];
let gameOverHistory = [];
const expectedGameOverHistory = [
  // Initial value (pre-move)
  false, 
  // values post moves
  false, false, false, false, false, 
  false, false, false, false, 
  false, false, 
  false, true,
  // After move h4, the game is over and remains over!
   true, true, true
];
gameOver$.subscribe(isGameOver => {
  gameOverHistory.push(isGameOver);
});
// Run the test
const noop = () => ({});
zip(from(shortestChessGame), interval(200), function(boardClick, b) {
  boardClicks.next(boardClick);
}).subscribe(noop, noop, () => {
  console.log(`gameOver$ : actual`, gameOverHistory);
  console.log(`gameOver$ : expected`);
  if (
    JSON.stringify(gameOverHistory) === JSON.stringify(expectedGameOverHistory)
  ) {
    console.log(`Test passed!`);
  } else {
    console.log(`Test failed!`);
  }
});

As a side bonus, we can actually see the game being played as the test proceeds:

As it is clear from the code, we simply pass the events (player clicks) and accumulate the history of the streams tested against, and compare that with the expected history. Note that we have tested (and visualized!) a user scenario without resorting to sophisticated end-to-end testing automation tools (like Cypress, or selenium). We also did not suffer from long-running tests. Actually, a 200ms delay was introduced between each click, so readers could follow the tests. Furthermore, there are no flaky tests --- we are testing pure functions!

While this is a simple example, the same conclusions generalize nicely to any user scenarios which can be written as a sequence of events. Functional UI paradigms are not rooted in some specific tooling or framework but in simple equations. This guarantees the applicability of the techniques across frameworks, languages, tooling, and target devices.

Conclusion

User interfaces are reactive systems and as such can be specified through their events/actions interface with the external systems of interest, and a pure reactive function mapping the actions of the user on the user interface to actions on the interfaced system. When appropriate, the reactive function can be replaced by a system of equations linking streams of events to streams of commands.

We identified in fine three functional UI strategies revolving around the fundamental equation of reactive systems. The three strategies are equivalent. However, in control-dominated or highly modal reactive systems (like games or embedded systems where the same event may trigger many different reactions), the state-machine-based modeling may provide a more concise, productive, and formally analyzable formulation of the specifications. Stream-based approaches may also be concise and elegant for a certain class of problems but their implementation in JavaScript is not devoid of accidental complexity. Conversely, the direct use of the fundamental equation of reactive systems is the simplest strategy -- it does not require a priori any abstractions beyond those offered by the particular target language.

Let's conclude with a summary of the benefits of Functional UI.

Simplicity

The advantages of simplicity cannot be overstressed. Simplicity has a positive impact on recruiting and training productive software engineers. Rather than looking for developers that are experts in framework X, it is enough to find developers that are experts in the target language -- that may be a larger pool to choose from. The UI framework can be chosen for its specific advantages today and changed with minimal cost tomorrow, when the specifications evolve or information accrues.

For instance, in the chess game example, React was used as a rendering engine rather than as a framework -- there was no need to be acquainted with Hooks, Context, Redux, Sagas, and many more libraries from the React ecosystem. We chose React because of the availability of a chess component that significantly reduced what we had to implement, and because its API was better than a competing Vue chess component. Should the requirements of the interface change (for instance to visualize the range of possible moves) in a way that the Vue chess component's API becomes more suitable, we can switch to that simply by adapting the props interface.

Separation of skills and teams

The advantages of the modularity and separation of concerns brought about by functional UIs are also significant. The logic of the application is cleanly separated from the rendering (and other effects) of the application. Chris Coyier alluded in The Great Divide that widely different skills are necessary to produce good-looking, engaging, and accessible web pages vs. implementing interactive applications with complex behavior in JavaScript. With Functional UI, developers and designers agree on an interface (for instance props). Then designers design, developers develop, both only meeting to discuss changes in the interface between them, mostly in response to changes in the application's specifications.

Cross-platform, cross-language, cross-device The reactive function being is a pure computation, it uses nothing specific to the underlying OS or device. Furthermore, Turing languages being equivalent, it should be possible to express the same reactive function in any of them, e.g. JavaScript, Rust, or Go. In particular, the reactive function can be written in JavaScript for the browser, transpiled in Java for Android, in C for embedded devices, or in C# for .NET applications. Just like a computer needs drivers specific to the hardware they operate, changing OS or devices will nonetheless require changing the command handlers. The reactive function, i.e. the application specifications, however, need not change.

Reliability

Getting specifications right is a stubbornly hard problem; getting specifications to be accurately understood by developers is another one. Functional UI reduces the gap between specification and implementation -- developers are directly coding user scenarios. This restricts and quickly surfaces bugs that are all the more costly as they are discovered late in the product lifecycle.

Functional UI strategies also lead to a more productive testing process. Developers may unit-test user scenarios. Tests can be fully run in parallel and are not burdened by complex and slow set-ups. The potential for flaky tests in testing a user scenario is eliminated. Integration tests and end-to-end tests can be reduced in importance, as the test pyramid methodology suggests. This is to be contrasted with an approach that is gaining adoption in the front-end world: Write tests. Not too many. Mostly integration. The latter approach is largely driven by the coupling imposed by the UI frameworks in use. That coupling makes it inconvenient, complex, or time-consuming to test reliably a user scenario without running the whole framework.

Ecosystem The relative newness of Functional UIs does not, counter-intuitively, lead to a lack of ecosystem. Because functions are the basic unit of composition, any functional library can be used and integrated easily. As such, API requests can be implemented indifferently with the native fetch/Axios/superagent if the details of the request are all behind a common interface.

UI libraries equally abound. UI frameworks still can be used -- as libraries, by using only their render function. However, a large, growing, standard-based set of web component libraries is used at scale by major companies (Microsoft's FAST, Salesforce's Lightning, ING's Lion among many others). The current trend is for these companies to move away from framework-specific UI libraries toward standards-based toolkits that can be amply customized by the user into full-fledged design systems. Thus, Microsoft's Fluent design system, previously implemented with React is now implemented with Microsoft's FAST foundation as a base. The same foundation is also used to implement the FAST design system. Similarly, users can implement their own design system on top of the foundation by adjusting stylesheets and CSS custom properties.

Furthermore, the large jQuery ecosystem that predates React and the other frameworks can be leveraged. The jQuery API has mostly been co-opted into the standard web API used in modern browsers. This means that you can get a jQuery date picker (a search on npm returned over 90 results), and replace the jQuery API with the equivalent web API with little effort. For instance, the following jQuery code $('selector') directly translates to the native document.querySelectorAll('selector').

The success of Elm on large-scale web applications in the course of its seven years of life proves the real-life validity of functional UI strategies. As a data point, the largest transport provider in Norway rewrote its website with 83,000 Lines of Elm. A lead developer reported that a team of interns picked up the language quickly and produced code that is still running in production today. Elm additionally touts its reliability with its no-runtime exception tagline.

With the appropriate tooling, developers can quickly learn and enjoy any functional UI approach; and produce high-quality, reliable software.

About the Author

Bruno Couriol holds a Msc in Telecommunications, a BsC in Mathematics and a MBA by INSEAD. Starting with Accenture, most of his career has been spent as a consultant, helping large companies addressing their critical strategical, organizational and technical issues. In the last few years, he developed a focus on the intersection of business, technology and entrepreneurship.

Rate this Article

Adoption
Style

BT