Data Driven UIs, Incrementally

Yaron Minsky / Jane Street Group

Your UI as two functions

Action

Model

Vdom

module type Component = sig
  type model
  type action

  val apply_action : model -> action -> model

  val render : model -> Vdom.t
end

Your UI as an Incremental Computation

Model

Vdom

DOM

Model'

Vdom'

DOM'

Incrementality with Incremental

  • Your computation as a dependency graph!
  • Based on Acar's Self Adjusting Computations

Map and Map2

Model

Sub 1

Sub 2

Vdom 1

Vdom 2

Vdom

val map  : 'a Incr.t -> ('a -> 'b) -> 'b Incr.t
val map2 : 
  'a Incr.t -> 'b Incr.t -> ('a -> 'b -> 'c) -> 'c Incr.t

Bind

Model

Sub 1

Sub 2

Vdom 1

Vdom 2

Vdom

val bind : 'a Incr.t -> ('a -> 'b Incr.t) -> 'b Incr.t

Incremental thus far

  • Map works for static structures
  • Bind adds (limited) dynamism
  • But how can we be dynamic and incremental?

Mapping over an incremental map

val incr_map
  :  ('k, 'v1, 'cmp) Map.t Incr.t
  -> ('v1 -> 'v2)
  -> ('k, 'v2, 'cmp) Map.t Incr.t

m

let incr_map m f =
  let%map m = m in
  Map.map m f

m'

Diffing Maps

val symmetric_diff
  :  ('k, 'v, 'cmp) Map.t
  -> ('k, 'v, 'cmp) Map.t
  -> data_equal:('v -> 'v -> bool)
  -> ('k * [ `Left of 'v
           | `Right of 'v
           | `Unequal of 'v * 'v ]) Sequence.t

Hooking in Diffs with diff_map

val diff_map
  :  'a Incr.t
  -> (('a * 'b) option -> 'a -> 'b)
  -> 'b Incr.t
let diff_map i f =
  let old = ref None in
  let%map a = i in
  let b = f !old a in
  old := Some (a, b);
  b

Implementing incr_map

let incr_map m f =
  diff_map m (fun old m ->
      match old with
      | None -> Map.map m ~f
      | Some (old_in, old_out) ->
        let diff =
          Map.symmetric_diff old_in m
            ~data_equal:phys_equal
        in
        Sequence.fold diff ~init:old_out
          ~f:(fun acc (key,change) ->
              match change with
              | `Left _ -> Map.remove acc key
              | `Unequal (_,data) | `Right data ->
                Map.set acc ~key ~data:(f data)))

Mapping an incremental function over an incremental map

val incr_map'
  :  ('k, 'v1, 'cmp) Map.t Incr.t
  -> ('v1 Incr.t -> 'v2 Incr.t)
  -> ('k, 'v2, 'cmp) Map.t Incr.t

Mapping an incremental function over an incremental map

m

e

e

e

e

e'

e'

e'

e'

m'

Split and Join

val split
  :  ('k,'v       ,'cmp) Map.t Incr.t
  -> ('k,'v Incr.t,'cmp) Map.t Incr.t
val join
  :  ('k,'v Incr.t,'cmp) Map.t Incr.t
  -> ('k,'v       ,'cmp) Map.t Incr.t
let incr_map' m f =
  join (incr_map (split m) f)

Extending Incremental

  • diff_map works on any diffable data structure
  • Split and Join, on top of Incremental.Expert
    • Incr_map provides Map-specific primitives
    • Incr_select for range and focus operations

Things to remember

  • UI design is an optimization problem
  • SAC is a powerful optimization tool
  • Diff and Patch are incremental functional glue

https://github.com/janestreet/incr_dom

https://opensource.janestreet.com

Beyond the browser

Server

Client

Full

model

Client

view

Model

Vdom

DOM

Full

model

Client

view

Model

Vdom

DOM

Data Driven UIs, Incrementally (Strangeloop 2018)

By Yaron Minsky

Data Driven UIs, Incrementally (Strangeloop 2018)

  • 410