It takes an integer variable and an expression and increments the variable each time the expression is evaluated, returning its value unchanged. In general, this combinator is not well-behaved: it changes the graph during its evaluation. The result depends on the evaluation order of expr and counter.
To detect these ambiguities, the current implementation maintains a damaged : bool field in Lwd nodes, to detect if a node has changed in the current update cycle.
However this was an conservative approximation: if any effect was detected during evaluation, the graph was considered ambiguous. The new implementation accepts graph if there is a strict ordering between read and write effects.
join now allows the outer computation to have effects that impacts the inner one. map2 is now evaluated left-to-right (in map2 f t1 t2 , t1 is evaluated before t2). Assuming t2 is not used elsewhere in the graph (there are no aliases outside), then t1 is allowed to perform effects that can invalidate t2.
Make reasoned use of this property. Relying on the ordering of effects is a bad practice in general, and not really in the spirit of Lwd; yet, it enables efficient implementation of a few specialized operations.
Example: sharing constraints and cutting-off evaluation
Imagine we want to make a list of widgets that are constrained to all have the same width.
This definition will produce the right Ui but has a performance bug. When a single element of the table changes, the width is updated and the final layout is entirely recomputed.
An $O(1)$ change (a single item) turns into an $o(n)$ recomputation (layout of each item).
What we would like is a cutoff operator: if after recomputation the width value is the same, we can skip the relaying-out the table. However a performant cutoff operator is tricky to implement and can have surprising runtime characteristics.
The evaluation and damage-resilience of join let us implement a good one:
valoften_changing:floatLwd.tvaltransform:float->ui(* We have a floating point computation that changes often and we would like to
apply a transformer on it.
However, the transformation is expensive and cares only about the integral
part of its input, so we would like to avoid recomputing it when possible.
*)letcross_thresholdv1v2=int_of_floatv1<>int_of_floatv2(* The expensive graph that is always recomputed *)letunfiltered=Lwd.maptransformoften_changing(* A cheaper graph that detects integral changes for recomputing the transform *)letfiltered=cutoffoften_changingcross_threshold(Lwd.maptransform)