Module Unicorn_jsoo

Unicorn provides a small algebra of seven combinators to define GUI applications:

These combinators are the glue to assemble complex applications out of smaller widgets. The jsoo (js_of_ocaml) backend additionnally provides the primitive widgets and HTML support.

type !'a t

The type of a widget that can render and update values of type 'a.

val run : ?id:string -> 'a t -> 'a -> unit

run w x renders the widget w with the initial state x, then proceed to update it in reaction to user events. It's the main loop that drives the GUI application, so you probably want to call it only once.

  • If ?id is specified, the widget renders in the HTML node with the corresponding id attribute. Otherwise, the widget is appended to the HTML body of the webpage.

Algebra

val empty : 'a t

The empty widget, invisible and ineffective.

val (&) : 'a t -> 'a t -> 'a t

a & b displays both widgets a and b side by side.

  • identity: ∀ w. empty & w == w == w & empty
  • associative: ∀ a b c. a & (b & c) == a & b & c == (a & b) & c
  • NOT commutative, as b & a displays the widgets in a different order.

The polymorphic 'a in an 'a t widget is invariant: It is consummed when rendering, but also produced in reaction to user events. As such, we always need pair of functions to "map" over it:

val iso : ( 'a, 'b ) Optic.iso -> 'b t -> 'a t

iso i w maps the isomorphism i over the widget w.

  • identities: ∀ i. iso i empty == empty and ∀ w. map Iso.id w == w
  • composition: ∀ i j w. iso i (iso j w) == map (Iso.compose i j) w
  • distributive: ∀ i a b. iso i a & iso i b == iso i (a & b)
val on : ( 'a, 'b ) Optic.lens -> 'b t -> 'a t

on lens w zooms on a sub-field of the state 'a with the lens and renders/updates it with the widget w.

  • identities: ∀ lens. on lens empty == empty and ∀ w. on Lens.id w == w
  • composition: ∀ l1 l2 w. on l1 (on l2 w) == on (Lens.compose l1 l2) w
  • distributive: ∀ lens a b. on lens a & on lens b == on lens (a & b)
val into : ( 'a, 'b ) Optic.prism -> 'b t -> 'a t

into prism w pattern matches the state 'a with the prism, to render the widget w. If the prism fails, then it renders empty.

  • identities: ∀ prism. into prism empty == empty and ∀ w. into Prism.id w == w
  • composition: ∀ p1 p2 w. into p1 (on p2 w) == into (Prism.compose p1 p2) w
  • distributive: ∀ prism a b. into prism a & into prism b == into prism (a & b)
val case : ( 'a, 'b ) Optic.prism -> 'b t -> 'a t

case prism w is the same as into prism w, but the internal state of the widget w is reset when the prism is not satisfied.

  • definition: ∀ p w. into p w == stateful w (case Prism.(product id p) dynamic)
val stateful : 's -> ('s * 'a) t -> 'a t

stateful s w encapsulates the internal state of the widget w with an initial value s.

  • identities: ∀ s. stateful s empty == empty and ∀ s w. stateful s (on Lens.snd w) == w
  • composition: ∀ s1 s2 w. stateful s1 (stateful s2 w) == stateful (s1, s2) (iso Iso.assoc w)
  • distributive: ∀ s a b. stateful s a & b == stateful s (a & on Lens.snd b) and ∀ s a b. a & stateful s b == stateful s (on Lens.snd a & b)
  • commutative: ∀ i s w. iso i (stateful s w) = stateful s (iso (Iso.product id i) w) and same for lenses and prisms.

However, internal state is not shared: stateful s a & stateful s b =/= stateful s (a & b).

val dynamic : ('a t * 'a) t

dynamic renders and updates a dynamic widget coming from the state.

  • ∀ w. w == stateful w dynamic

Useful combinators

The following are defined from the previous combinators.

val of_list : 'a t list -> 'a t

of_list ws displays the static list of widget ws.

val list : 'a t -> 'a list t

list w displays a dynamic list, using the widget w for each element.

val (<*>) : 'a t -> 'b t -> ('a * 'b) t

a <*> b is the product of the two widgets a and b.

val cond : ( 'a -> bool ) -> 'a t -> 'a t

cond p w renders the widget w when the predicate p is satisfied.

  • identities: ∀ p. cond p empty == empty and ∀ w. cond (fun _ -> true) w == w and ∀ w. cond (fun _ -> false) w == empty
  • composition: ∀ p q w. cond p (cond q w) == cond (fun x -> p x && q x) w
  • distributive: ∀ p a b. cond p (a & b) == cond p a & cond p b
val ifte : ( 'a -> bool ) -> 'a t -> 'a t -> 'a t

ifte p if_true if_false renders the widget if_true when p is satisfied, otherwise if_false.

val cond_forget : ( 'a -> bool ) -> 'a t -> 'a t

Same as cond, but the internal state is lost when the predicate is unsatisfied (see case).

val stateful_by : ( 'a -> 's ) -> ('s * 'a) t -> 'a t

stateful_by f w is the same as stateful, but the initial value is computed from the current state.

Recursive definitions

As OCaml is a strict language, recursive definitions need special support as this would fail to terminate:

let rec list w = into Prism.cons (w <*> list w)

and one should rather delay slightly the recursion:

let rec list w = into Prism.cons (w <*> apply list w)
val of_lazy : 'a t Stdlib.Lazy.t -> 'a t

of_lazy w delays the lazy widget w, such that of_lazy (lazy w) == w

val apply : ( 'a -> 'b t ) -> 'a -> 'b t

apply f x is of_lazy (lazy (f x)).

val fix : ( 'a t -> 'a t ) -> 'a t

fix f uses of_lazy to simplify the definition of recursive traversals.

HTML primitive widgets

val str : string -> 'a t

str s displays the constant string s as HTML.

val text : ( 'a -> string ) -> 'a t

text f renders a dynamic string using the function f on the current state.

val checkbox : bool t

checkbox is a primitive widget to render and edit a boolean.

val input_int : int t

input_int is a primitive widget to render and edit an integer as a text input.

val input_string : string t

input_string is a primitive widget to render and edit a single-line string.

val button : 'a t -> 'a t

button w is an HTML button using the widget w for its contents. The reaction when the button is clicked can be specified with E.click:

button (str "Click" & E.click (fun old_state -> new_state))
module H : sig ... end

HTML elements and nodes, like div, span, etc.

module A : sig ... end

HTML attributes and properties.

module E : sig ... end

HTML events.