Abstract: Most modern UIs are implementing using a reactive programming model. Reactive UIs today rely on runtime instrumentation to propagate effects to the screen, which incurs a cost of latency, memory usage, and code size. I propose to use static analysis and optimization to minimize these costs, producing optimal reactive interfaces.

Motivation

This problem requires a bit of background if you’re not steeped in UI programming lore, so bear with me for a moment.

Reactive UI programming

The foundation of most user interfaces is an object-oriented, imperative API such Javascript’s DOM. A well-established lesson from UI programming is that building interfaces with these APIs is annoying and error-prone. For example, say you wanted to make a simpler counter: a button and a number indicating the number of presses. Using vanilla Javascript, that component would look like this:

export let VanillaCounter = () => {
  let n = 0;
  let div = document.createElement("div");
  let btn = document.createElement("button");
  btn.innerText = "+";
  let span = document.createElement("span");
  span.innerText = n.toString();
  btn.addEventListener("click", () => {
    n += 1;
    span.innerText = n.toString();
  });
  div.append(btn, span);
  return div;
};

A particular issue with this code is that the click event handler is responsible both for updating the component’s state (n += 1) and for updating the component’s view (span.innerText = …). You can probably imagine that as your UI gets more complicated, it’s easy to make bugs like not updating all parts of your view that depend on a given piece of state.

Modern UI frameworks address this problem by separating out the state and the view, and automatically handling view updates when the state changes. For example, you can implement the counter in React like this:

export let ReactCounter = () => {
  let [n, setN] = useState(0);
  let onClick = () => setN(n + 1);
  return <div>
    <button onClick={onClick}>+</button>
    <span>{n}</span>
  </div>
};

Here, useState is a construct provided by React. React understands that when onClick calls setN, then the component needs to re-render with the new value of n.

You’ll find a similar reactivity model in SwiftUI, used to implement iOS apps:

struct Counter: View {
  @State private var n = 0
  
  var body: some View {
    HStack {
      Button("+") { n += 1 }
      Text("\\(n)")
    }
  }
}

As well as Jetpack Compose, used to implement Android apps:

@Composable
fun Counter() {
  var n by remember { mutableStateOf(0) }
  
  Row() {
    Button(onClick = { n++ }) { Text("+") }
    Text("$n")
  }
}

Some languages / frameworks veer away from effects and towards pure functional programming, such as Elm’s message-based updates:


type alias Model = Int

init : () -> ( Model, Cmd Msg )
init _ = ( 0, Cmd.none )

type Msg = Increment

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model = case msg of
    Increment -> (model + 1, Cmd.none)

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Increment ] [ text "+" ]
        , span [] [ text (String.fromInt model) ]
        ]

Bonsai uses a similar style:

module Action = struct
  type t =
    | Increment
  [@@deriving sexp]
  
  let apply _ n = function
	  | Increment -> n + 1
end

let bonsai_counter graph =
  let state, inject =
    Bonsai.state_machine graph
      ~sexp_of_model:[%sexp_of: Int.t]
      ~equal:[%equal: Model.t]
      ~sexp_of_action:[%sexp_of: Action.t]
      ~default_model:0
      ~apply_action:Action.apply
  in
  let%arr n and inject in
  let on_click _ = inject Incr in
  Node.div 
	  [ Node.button ~attrs:[Attr.on_click on_click] [ Node.text "+" ]
	  , Node.textf "%d" n ]
;;

And Flapjax has an idiosyncratic framework of events & behaviors:

export let FlapjaxCounter = () => {
  let btn = BUTTON({}, "+");
  let countB = clicksE(btn)
    .collectE(0, (_e, n) => n + 1)
    .startsWith(0);
  return DIV({}, btn, SPAN({}, countB));
};