Imperative.js is a new entrant to the crowded space of frontend UI frameworks. It uses ES6 generator syntax to enable straightforward, imperative-style code. Here is a simple example to illustrate what programming is like in Imperative.js.

const I = require("imperative")
I.run(main(), document.body)

function* main() {
  let ok = false;
  let str = "";
  while(!ok) {
    yield* I.h('div', {}, [
      function*() {
        str = yield* input(str)
      }(),
      function*() {
        yield* button("OK")
        ok = true;
      }()
    ])
  }
  yield () => I.h('div', {}, str)
}

function* button(str) {
  return yield (resolve,reject) =>
    I.h('button', {
      props: {type: 'text'},
      on: {click: () => resolve()}
    }, str)
}

function* input(str) {
  return yield (resolve,reject) =>
    I.h('input', {
      props: {type: 'text', value: str},
      on: {input: e => resolve(e.target.value)}
    })
}

The advantages of Imperative over other frontend UI frameworks include a simple implementation, compositional semantics, and exceptional flexibility.

The core framework is extremely small. Imperative.h uses the hyperscript convention of snabbdom, Imperative’s only dependency. h is overloaded to operate on either virtual-dom elements or virtual-dom widgets. A widget is just a generator which yields values of the type (resolve, reject) => virtual-dom, as in the Button and Input widgets above.

When you call h with widgets as children, you get back a widget which composes the two widgets; it will return the value of the first child that returns. You can run a widget with Imperative.run(widget, element).

Using h will tie the state of your application to the DOM tree, similarly to other frameworks like React and Angular. However, unlike those frameworks Imperative has the ability to output other types than a DOM tree; you can yield a custom output type from your widgets and compose it using the low-level primitives Imperative.mapOut and Imperative.zip. You can also mix custom output types with DOM output.

Comparison to Other Approaches

Although there are a great many frontend UI frameworks, most of them fall into a few types. The modern GUI was, not coincidentally, invented alongside object-oriented programming at Xerox PARC, and this remains one of the dominant UI conventions: stateful UI widgets which communicate with each other by sending messages. A second convention is even older, and can be seen in old terminal UIs like vi or top: UI state is held globally, and the view is computed based on the state. A more recent approach is functional reactive programming, in which the UI is seen as a sort of time-dependent function Events -> Output, and it is composed out of simpler time-dependent transformations using stateless combinators.

The problem with the object-oriented UI is that the state of the program is distributed among many independent objects. Inevitably, there are a large number of hidden dependencies between the state of different objects; these dependencies are not encoded in the program structure, are not enforced, and so are easy to violate, leading to bugs. As an object-oriented UI grows large, its interactions become unpredictable and almost impossible to analyze, as one has to keep track of all relevant state variables.

Imperative.js solves this problem with old-fashioned imperative-style structured programming: state variables are generally bound to a particular scope, and are updated only at assignment statements, making analysis far easier.

The problem with the global state-based UI is that, in a modern GUI, multiple components should have independent and encapsulated state. For example, two tree views in a window will each have a number of state variables indicating scroll progress, which nodes are open or closed, etc. The global state approach does not directly enable encapsulation of this local state. When one begins building infrastructure on top of the naive global-state approach (Redux etc), one is led to reinvent objects with all their downsides.

Imperative.js solves this problem by augmenting traditional structured programming with the zip operation which lets two components evolve in parallel. This introduces a managed degree of nondeterminism and local state, to allow for user action, while avoiding the extremes of a global state store or a flat network of objects.

Functional reactive programming is the most unique approach. I think it works well for some scenarios but its vocabulary doesn’t work well for typical UI development. To avoid explicit state and imperative code makes sense when you are writing a data transformation: state variables are incidental to the definition of, say, the nth Fibonacci number. But state and temporal progress are intrinsic to UI, and are a natural way of describing many UI interactions. A simple interaction like a multi-page “wizard” is a puzzle in the FRP paradigm; in Imperative.js it is just a sequence of (yield*) function calls in a plain one-to-one correspondence with the sequence of pages.

Further Reading