I'm trying to control update propagation of views so that update only occurs when the value has changed.
Here is an example. I am dragging the mouse and I want to convert a view of XY positions into a view of distances from the mouse-down position, where these distances are snapped to the nearest value of 10.
let snap (dist: float) x = dist * Math.Round (x / dist)
let inline private sq x = x * x
let dist (stX, stY) (enX, enY) =
sq (stX - enX) + sq (stY - enY) |> float |> sqrt |> snap 10
// set the value only if it has changed
let inline assign (var: Var<_>) value = if value <> var.Value then var.Value <- value
let distView down pos = // result is View<float>
let state = Var.Create 0.
pos // input is View<int * int>
|> View.Bind (fun pos ->
assign state (dist down pos)
state.View)
|> View.SnapshotOn state.Value state.View
As you drag away from the mouse-down position, many XY positions will produce the first snapped distance, 0, and then many more XY positions will produce the next distance, 10, and then 20, and so on.
The usual behavior of computed views (View.Map
, View.Bind
, etc.) is that whenever input views are updated (their Snap
becomes obsolete), this marks the computed view snap as obsolete, and so on, all the way up the dependency tree. A new snap value will be recalculated if and when it is next demanded.
This is even true for View.UpdateWhile
. When the first view is false, the computed view will nevertheless update each time the second view is updated, and return the last captured value of the second view over and over.
This is fine in that the values are correct, but if the calculation done inside a View.Map
function is expensive, we don't want to repeat the calculation when the value hasn't changed. In my case, the calculation done with the distance (not shown here) is expensive, but this needs to be efficient since it is going on while the user is dragging the mouse.
In the code above, at first glance it looks like View.Bind
returns state.View
, which doesn't change all the time, but in fact View.Bind
creates a new computed view, which just happens to have the value of state.View
in this example. So even if state.View
hasn't changed, View.Bind
will create a new snap for each new XY position, copying the value for this snap out of state.View
.
So my idea was to use View.SnapshotOn
at the end, so that the resulting view is only updated when state.View
has actually changed value. But sadly it doesn't work, and I only get the default value.
I believe the problem might be in Snap.SnapshotOn
. Here's the code:
let SnapshotOn sn1 sn2 =
match sn1.State, sn2.State with
| _, Forever y -> CreateForever y
| _ ->
let res = Create ()
let v = ref None
let triggered = ref false
let obs () =
v := None
MarkObsolete res
let cont () =
if !triggered then
match !v with
| Some y when IsForever sn2 -> MarkForever res y
| Some y -> MarkReady res y
| _ -> ()
When sn1 (fun x -> triggered := true; cont ()) obs
When sn2 (fun y -> v := Some y; cont ()) ignore
res
I'm not sure what this triggered
test is all about, but it might be the problem. As I see it, we want the value from sn2
to be applied to res
, and we want res
to be made obsolete when sn1
becomes obsolete.
Perhaps I don't understand the code. Am I misusing View.SnapshotOn
or is there a bug?