@@ -90,26 +90,6 @@ Here `Lwd.map : ('a -> 'b) -> 'a Lwd.t -> 'b Lwd.t` apply a transformation to a | |||
When the `Link` is triggered, the counter is incremented. Because `document` depends on the value of the counter it is invalidated. | |||
#### Optional: abstracting local state | |||
This pattern of having local state that you want to manipulate in an almost purely functional way is very common and has been abstracted in the `Lwd_utils` library. | |||
Here is another way to implement our button example: | |||
```ocaml | |||
val Lwd_utils.local_state : ('a Lwd.t -> ('a -> unit) -> 'a * 'b) -> 'b | |||
Lwd_utils.local_state (fun counter update -> | |||
let initial_value = 0 in | |||
let increment clicks () = update (clicks + 1) in | |||
let button clicks = | |||
Link (increment clicks, | |||
Text ("Clicked " ^ string_of_int clicks ^ " times")) | |||
in | |||
initial_value, Lwd.map button counter | |||
) | |||
``` | |||
### Building computation graph | |||
`Lwd.t` implements a few abstractions that should be familiar to seasoned functional programmers: | |||
@@ -215,4 +195,4 @@ The first question can be answered positively with a naive encoding: put `Lwd.va | |||
To answer the second question, it is interesting to observe that there is no concept of "diffing" here. _Lwd_ does not try to see if things have changed in order to update them. Rather, if an input change, the whole branch that depends on it is recomputed. | |||
While this might lead to inefficient recomputations. ...TODO... | |||
While this might lead to inefficient recomputations. ...TODO... |
@@ -1,6 +1,14 @@ | |||
val (let$) : 'a Lwd.t -> ('a -> 'b) -> 'b Lwd.t | |||
(** Alias to {!Lwd.map'} suitable for let-op bindings *) | |||
val (let$*) : 'a Lwd.t -> ('a -> 'b Lwd.t) -> 'b Lwd.t | |||
(** Alias to {!Lwd.bind} suitable for let-op bindings *) | |||
val (and$) : 'a Lwd.t -> 'b Lwd.t -> ('a * 'b) Lwd.t | |||
(** Alias to {!Lwd.pair} suitable for let-op bindings *) | |||
val ($=) : 'a Lwd.var -> 'a -> unit | |||
(** Infix alias to {!Lwd.set} *) | |||
val ($<-) : 'a Lwd_table.row -> 'a -> unit | |||
(** Infix alias to {!Lwd_table.set} *) |
@@ -482,10 +482,10 @@ let fold_monoid map (zero, reduce) seq = | |||
let monoid = (empty, concat) | |||
let bind_list ls f = | |||
let transform_list ls f = | |||
Lwd_utils.map_reduce f monoid ls | |||
let of_list ls = bind_list ls element | |||
let of_list ls = transform_list ls element | |||
let rec of_sub_array f arr i j = | |||
if j < i then empty | |||
@@ -494,9 +494,9 @@ let rec of_sub_array f arr i j = | |||
let k = i + (j - i) / 2 in | |||
concat (of_sub_array f arr i k) (of_sub_array f arr (k + 1) j) | |||
let bind_array arr f = of_sub_array f arr 0 (Array.length arr - 1) | |||
let transform_array arr f = of_sub_array f arr 0 (Array.length arr - 1) | |||
let of_array arr = bind_array arr element | |||
let of_array arr = transform_array arr element | |||
let to_list x = | |||
let rec fold x acc = match x with | |||
@@ -1,54 +1,46 @@ | |||
(* Sequence construction | |||
(** {0 Sequence manipulation} | |||
[Lwd_seq] implements a type of ordered collections with a pure interface. | |||
In addition, changes to collections are easy to track. | |||
[Lwd_seq] is an ordered collection with a pure interface. | |||
Changes to collections are easy to track. | |||
A collection can be transformed with the usual map, filter and fold | |||
combinators. If later, the transformation is applied again to an updated | |||
collection, shared elements (in the sense of physical sharing), the | |||
result of the previous transformation will be reused for these elements. | |||
A collection can be transformed with the usual map, filter and fold | |||
combinators. If the collection is updated, shared elements (in the sense of | |||
physical sharing), the result of the previous transformation will be reused | |||
for these elements. | |||
The book-keeping overhead is O(n) in the number of changes, so O(1) per | |||
element. | |||
The book-keeping overhead is O(n) in the number of changes, so O(1) per | |||
element. | |||
*) | |||
type +'a t | |||
type +'a seq = 'a t | |||
(** The type of sequences *) | |||
(* A sequence with no element. *) | |||
val empty : 'a seq | |||
(* A singleton sequence. The physical identity of the element is considered | |||
when reusing previous computations. | |||
(** {1 Primitive constructors} *) | |||
If you do: | |||
let x1 = element x | |||
let x2 = element x | |||
val empty : 'a seq | |||
(** A sequence with no element. *) | |||
Then x1 and x2 are seen as different elements and no sharing will be done | |||
during transformation. | |||
*) | |||
val element : 'a -> 'a seq | |||
(** A singleton sequence. The physical identity of the element is considered | |||
when reusing previous computations. | |||
(* Concatenate two sequences into a bigger one. | |||
As for [element], the physical identity of a sequence is considered for | |||
reuse. | |||
*) | |||
val concat : 'a seq -> 'a seq -> 'a seq | |||
If you do: | |||
val monoid : 'a t Lwd_utils.monoid | |||
val lwd_monoid : 'a t Lwd.t Lwd_utils.monoid | |||
{[let x1 = element x | |||
let x2 = element x]} | |||
(*val bind : 'a seq -> ('a -> 'b seq) -> 'b seq*) | |||
val bind_list : 'a list -> ('a -> 'b seq) -> 'b seq | |||
val bind_array : 'a array -> ('a -> 'b seq) -> 'b seq | |||
Then [x1] and [x2] are seen as different elements and no sharing will be | |||
done during transformation. | |||
*) | |||
val of_list : 'a list -> 'a seq | |||
val of_array : 'a array -> 'a seq | |||
val to_list : 'a seq -> 'a list | |||
val to_array : 'a seq -> 'a array | |||
val concat : 'a seq -> 'a seq -> 'a seq | |||
(** Concatenate two sequences into a bigger one. | |||
As for [element], the physical identity of a sequence is considered for | |||
reuse. | |||
*) | |||
(* Look at the contents of a sequence *) | |||
(** {1 Looking at sequence contents} *) | |||
type ('a, 'b) view = | |||
| Empty | |||
@@ -56,18 +48,44 @@ type ('a, 'b) view = | |||
| Concat of 'b * 'b | |||
val view : 'a seq -> ('a, 'a seq) view | |||
(** View how a sequence is defined *) | |||
(** {1 Conversion between sequences, lists and arrays} *) | |||
val transform_list : 'a list -> ('a -> 'b seq) -> 'b seq | |||
(** Produce a sequence by transforming each element of a list and concatenating | |||
all results. *) | |||
val transform_array : 'a array -> ('a -> 'b seq) -> 'b seq | |||
(** Produce a sequence by transforming each element of an array and | |||
concatenating all results. *) | |||
val of_list : 'a list -> 'a seq | |||
(** Produce a sequence from a list *) | |||
val of_array : 'a array -> 'a seq | |||
(** Produce a sequence from an array *) | |||
val to_list : 'a seq -> 'a list | |||
(** Produce a list from a sequence *) | |||
val to_array : 'a seq -> 'a array | |||
(** Produce an array from a sequence *) | |||
(** {1 Balanced variant of sequences *) | |||
module Balanced : sig | |||
(* A variant of the sequence type that guarantees that the depth of | |||
transformation, as measured in the number of [concat] nodes, grows in | |||
O(log n) where n is the number of elements in the sequnce. | |||
(** A variant of the sequence type that guarantees that the depth of a | |||
transformation, measured as the number of nested [concat] nodes, grows in | |||
O(log n) where n is the number of elements in the sequnce. | |||
This is useful to prevent stack overflows and to avoid degenerate cases | |||
where a single element change, but it is at the end of a linear sequence | |||
where a single element changes, but it is at the end of a linear sequence | |||
of [concat] nodes, thus making the total work O(n). | |||
For instance, in: | |||
[concat e1 (concat e2 (concat e3 (... (concat e_n))...))] | |||
{[concat e1 (concat e2 (concat e3 (... (concat e_n))...))]} | |||
If [e_n] changes, the whole spine has to be recomputed. | |||
@@ -79,7 +97,10 @@ module Balanced : sig | |||
only useful to balance the first sequence of the pipeline. Derived | |||
sequence will have a depth bounded by the depth of the first one. | |||
*) | |||
type 'a t = private 'a seq | |||
(** Type of balanced sequences *) | |||
val empty : 'a t | |||
val element : 'a -> 'a t | |||
val concat : 'a t -> 'a t -> 'a t | |||
@@ -87,38 +108,51 @@ module Balanced : sig | |||
val view : 'a t -> ('a, 'a t) view | |||
end | |||
(* Lwd interface. | |||
(** {1 Transforming sequences} *) | |||
(** | |||
All sequences live in [Lwd] monad: if a sequence changes slightly, parts | |||
that have not changed will not be re-transformed. | |||
*) | |||
(* [fold ~map ~reduce] transforms a sequence. | |||
If the sequence is non-empty, the [map] function is applied to element nodes | |||
and the [reduce] function is used to combine transformed concatenated nodes. | |||
If the sequence is empty, None is returned. | |||
*) | |||
val fold : | |||
map:('a -> 'b) -> reduce:('b -> 'b -> 'b) -> 'a seq Lwd.t -> 'b option Lwd.t | |||
(** [fold ~map ~reduce] transforms a sequence. | |||
If the sequence is non-empty, the [map] function is applied to element | |||
nodes and the [reduce] function is used to combine transformed concatenated | |||
nodes. | |||
If the sequence is empty, None is returned. | |||
*) | |||
val fold_monoid : | |||
('a -> 'b) -> 'b Lwd_utils.monoid -> 'a seq Lwd.t -> 'b Lwd.t | |||
(** Like [fold], but reduction and default value are defined by a [monoid] *) | |||
(* [map f] transforms a sequence by applying [f] to each element. *) | |||
val map : | |||
('a -> 'b) -> 'a seq Lwd.t -> 'b seq Lwd.t | |||
(** [map f] transforms a sequence by applying [f] to each element. *) | |||
val filter : | |||
('a -> bool) -> 'a seq Lwd.t -> 'a seq Lwd.t | |||
(** [filter p] transforms a sequence by keeping elements that satisfies [p]. *) | |||
val filter_map : | |||
('a -> 'b option) -> 'a seq Lwd.t -> 'b seq Lwd.t | |||
(** Filter and map elements at the same time *) | |||
val lift : 'a Lwd.t seq Lwd.t -> 'a seq Lwd.t | |||
(** Remove a layer of [Lwd] inside a sequence. *) | |||
val bind : 'a seq Lwd.t -> ('a -> 'b seq) -> 'b seq Lwd.t | |||
(** Sequence forms a monad too... *) | |||
val monoid : 'a t Lwd_utils.monoid | |||
(** Monoid instance for sequences *) | |||
val lwd_monoid : 'a t Lwd.t Lwd_utils.monoid | |||
(** Monoid instance for reactive sequences *) | |||
(* Low-level interface *) | |||
(** {1 Low-level interface for observing changes} *) | |||
module Reducer : sig | |||
(* The interface allows to implement incremental sequence transformation | |||
@@ -1,29 +1,102 @@ | |||
(** {0 Table manipulation} | |||
[Lwd_table] is an ordered collection with an impure interface. | |||
It is designed to be efficient in an interactive setting. | |||
The interface mimics the one of a doubly-linked lists: from a node, called | |||
row, you can iterate backward and forward, insert and delete other nodes, | |||
and change the value it is bound to. | |||
The sequence of nodes can be observed by map/reduce operations, that will | |||
be recomputed efficiently when sequence changes. | |||
*) | |||
type 'a t | |||
type 'a row | |||
(** The type of tables *) | |||
val make : unit -> 'a t | |||
val clear : 'a t -> unit | |||
(** Create a new table *) | |||
(** {1 Inserting rows} *) | |||
val prepend : ?set:'a -> 'a t -> 'a row | |||
(** Insert and return a new row at the start of a table. | |||
It can be optionnally initialized to the value of [set]. *) | |||
val append : ?set:'a -> 'a t -> 'a row | |||
(** Insert and return a new row at the end of a table. | |||
It can be optionnally initialized to the value of [set]. *) | |||
val prepend' : 'a t -> 'a -> unit | |||
(* Insert a new initialized row at start of a table *) | |||
val append' : 'a t -> 'a -> unit | |||
(* Insert a new initialized row at end of a table *) | |||
val before : ?set:'a -> 'a row -> 'a row | |||
(** Insert and return a new row just before an existing row. | |||
It can be optionnally initialized to the value of [set]. | |||
If the input row is unbound ([is_bound] returns false), the returned row is | |||
too. | |||
*) | |||
val after : ?set:'a -> 'a row -> 'a row | |||
(** Insert and return a new row just after an existing row. | |||
It can be optionnally initialized to the value of [set]. | |||
If the input row is unbound ([is_bound] returns false), the returned row is | |||
too. | |||
*) | |||
(** {1 Iterating over rows} *) | |||
val first : 'a t -> 'a row option | |||
(** Returns the first row of a table, or [None] if the table is empty *) | |||
val last : 'a t -> 'a row option | |||
(** Returns the last row of a table, or [None] if the table is empty *) | |||
val next : 'a row -> 'a row option | |||
(** Returns the row next to another one, or [None] if the input row is unbound | |||
or is the last row *) | |||
val prev : 'a row -> 'a row option | |||
(** Returns the row just before another one, or [None] if the input row is | |||
unbound or is the first row *) | |||
(** {1 Accessing and changing row contents} *) | |||
val get : 'a row -> 'a option | |||
(** Get the value associated with a row, if any, or [None] if the row is | |||
unbound *) | |||
val set : 'a row -> 'a -> unit | |||
(** Set the value associated with a row, or do nothing if the row is unbound *) | |||
val unset : 'a row -> unit | |||
(** Unset the value associated with a row *) | |||
(** {1 Removing rows} *) | |||
val is_bound : 'a row -> bool | |||
(** Returns [true] iff the row is bound in a table (it has not beem [remove]d | |||
yet, the table has not been [clear]ed) *) | |||
val remove : 'a row -> unit | |||
(** [remove] a row from its table, [is_bound] will be [true] after that *) | |||
val clear : 'a t -> unit | |||
(** Remove all rows from a table *) | |||
(** {1 Observing table contents} *) | |||
val reduce : 'a Lwd_utils.monoid -> 'a t -> 'a Lwd.t | |||
(** Observe the content of a table by reducing it with a monoid *) | |||
val map_reduce : ('a row -> 'a -> 'b) -> 'b Lwd_utils.monoid -> 'a t -> 'b Lwd.t | |||
(** Observe the content of a table by mapping and reducing it *) | |||
val iter : ('a -> unit) -> 'a t -> unit | |||
(** Immediate, non reactive, iteration over elements of a table *) |
@@ -17,7 +17,7 @@ let map_reduce inj (zero, plus) items = | |||
| (_,x) :: xs -> | |||
List.fold_left (fun acc (_, v) -> plus v acc) x xs | |||
let pure_pack monoid items = map_reduce (fun x -> x) monoid items | |||
let reduce monoid items = map_reduce (fun x -> x) monoid items | |||
let rec cons_lwd_monoid plus c xs v = | |||
match xs with | |||
@@ -37,21 +37,6 @@ let pack_seq (zero, plus) items = | |||
| (_,x) :: xs -> | |||
List.fold_left (fun acc (_, v) -> Lwd.map2 plus v acc) x xs | |||
let local_state f = | |||
let r = ref None in | |||
let acquire () = match !r with | |||
| None -> invalid_arg "Lwd_utils.trace: cyclic evaluation" | |||
| Some v -> v | |||
in | |||
let prim = Lwd.prim ~acquire ~release:ignore in | |||
let update v = | |||
r := Some v; | |||
Lwd.invalidate prim | |||
in | |||
let v, result = f (Lwd.get_prim prim) update in | |||
r := Some v; | |||
result | |||
let rec map_l (f:'a -> 'b Lwd.t) (l:'a list) : 'b list Lwd.t = | |||
match l with | |||
| [] -> Lwd.return [] | |||
@@ -1,14 +1,36 @@ | |||
type 'a monoid = 'a * ('a -> 'a -> 'a) | |||
(** A monoid, defined by a default element and an associative operation *) | |||
val lift_monoid : 'a monoid -> 'a Lwd.t monoid | |||
(** Use a monoid inside [Lwd] *) | |||
(** {1 List reduction functions} | |||
All reductions are balanced, relying on operator associativity. | |||
While [fold_{left,right}] would compute a chain like: | |||
[fold f [a; b; c; d] = f a (f b (f c d)] | |||
[reduce] uses tree-shaped computations like: | |||
[reduce f [a; b; c; d] = f (f a b) (f c d)] | |||
The depth of the computation grows in O(log n) where n is the length of the | |||
input sequence. | |||
*) | |||
val pack : 'a monoid -> 'a Lwd.t list -> 'a Lwd.t | |||
(** Reduce a list of elements in [Lwd] monad *) | |||
val pack_seq : 'a monoid -> 'a Lwd.t Seq.t -> 'a Lwd.t | |||
val pure_pack : 'a monoid -> 'a list -> 'a | |||
(** Reduce an (OCaml) [Seq.t] with a monoid *) | |||
val reduce : 'a monoid -> 'a list -> 'a | |||
(** Reduce a list with a monoid **) | |||
val map_reduce : ('a -> 'b) -> 'b monoid -> 'a list -> 'b | |||
(** Map and reduce a list with a monoid **) | |||
val local_state : ('a Lwd.t -> ('a -> unit) -> 'a * 'b) -> 'b | |||
(** {1 Other Lwd list functions} *) | |||
val map_l : ('a -> 'b Lwd.t) -> 'a list -> 'b list Lwd.t | |||
val flatten_l : 'a Lwd.t list -> 'a list Lwd.t | |||
(** Commute [Lwd] and [list] *) |
@@ -618,11 +618,11 @@ let grid | |||
Ui.resize ~w:col_widths.(i) ~h:row_h ?crop ?fill ?bg c) | |||
row | |||
in | |||
Lwd_utils.pure_pack pack_pad_x row) | |||
Lwd_utils.reduce pack_pad_x row) | |||
rows | |||
in | |||
(* TODO: mouse and keyboard handling *) | |||
let ui = Lwd_utils.pure_pack pack_pad_y rows in | |||
let ui = Lwd_utils.reduce pack_pad_y rows in | |||
Lwd.return ui | |||
let button ?attr s f = | |||
@@ -318,9 +318,9 @@ struct | |||
let pack_y = (empty, join_y) | |||
let pack_z = (empty, join_z) | |||
let hcat xs = Lwd_utils.pure_pack pack_x xs | |||
let vcat xs = Lwd_utils.pure_pack pack_y xs | |||
let zcat xs = Lwd_utils.pure_pack pack_z xs | |||
let hcat xs = Lwd_utils.reduce pack_x xs | |||
let vcat xs = Lwd_utils.reduce pack_y xs | |||
let zcat xs = Lwd_utils.reduce pack_z xs | |||
let has_focus t = Focus.has_focus t.focus | |||