Browse Source

updating doc

pre-release
Frédéric Bour 4 months ago
parent
commit
a9095faa6f
9 changed files with 199 additions and 97 deletions
  1. +1
    -21
      README.md
  2. +8
    -0
      lib/lwd/lwd_infix_letop.mli
  3. +4
    -4
      lib/lwd/lwd_seq.ml
  4. +82
    -48
      lib/lwd/lwd_seq.mli
  5. +74
    -1
      lib/lwd/lwd_table.mli
  6. +1
    -16
      lib/lwd/lwd_utils.ml
  7. +24
    -2
      lib/lwd/lwd_utils.mli
  8. +2
    -2
      lib/nottui-widgets/nottui_widgets.ml
  9. +3
    -3
      lib/nottui/nottui.ml

+ 1
- 21
README.md View File

@@ -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...

+ 8
- 0
lib/lwd/lwd_infix_letop.mli View File

@@ -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} *)

+ 4
- 4
lib/lwd/lwd_seq.ml View File

@@ -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


+ 82
- 48
lib/lwd/lwd_seq.mli View File

@@ -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


+ 74
- 1
lib/lwd/lwd_table.mli View File

@@ -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 *)

+ 1
- 16
lib/lwd/lwd_utils.ml View File

@@ -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 []


+ 24
- 2
lib/lwd/lwd_utils.mli View File

@@ -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] *)

+ 2
- 2
lib/nottui-widgets/nottui_widgets.ml View File

@@ -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 =


+ 3
- 3
lib/nottui/nottui.ml View File

@@ -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



Loading…
Cancel
Save