Equality
GraphOptions.Equality sets the default value comparison for signals and memos in the graph. Individual nodes can override it with a typed comparer.
JsIdentityPolicy, the default, follows JavaScript===.StructuralPolicyusesEqualityComparer<'T>.Default.
How the policies compare values on .NET
| Value kind | JsIdentityPolicy (default) | StructuralPolicy |
|---|---|---|
| Primitives, strings, structs, struct tuples | By value | By value |
Records, unions, Some x, other reference types | By reference: an equal copy propagates | By value: an equal copy is cut off |
None over None | Cut off (None is null) | Cut off |
nan over nan | Propagates (IEEE: nan equals nothing) | Cut off |
0.0 over -0.0 | Cut off | Cut off |
Compare the policies on .NET
This example counts the effect runs caused by each write, excluding the initial run.
type Point = { X: int; Y: int }
let countWakes (options: GraphOptions) (first: 'T) (second: 'T) =
use graph = new Graph (options)
use _ = graph.Activate ()
let source = createSignal first
let mutable runs = 0
createEffect (fun () -> source.Value |> ignore; runs <- runs + 1)
source.Value <- second
runs - 1
let structural = { GraphOptions.Default with Equality = StructuralPolicy () }
let equalityRows =
[ "equal record", countWakes GraphOptions.Default { X = 1; Y = 2 } { X = 1; Y = 2 },
countWakes structural { X = 1; Y = 2 } { X = 1; Y = 2 }
"Some 1 over Some 1", countWakes GraphOptions.Default (Some 1) (Some 1),
countWakes structural (Some 1) (Some 1)
"None over None", countWakes GraphOptions.Default (None: int option) None,
countWakes structural (None: int option) None
"nan over nan", countWakes GraphOptions.Default nan nan, countWakes structural nan nan
"0.0 over -0.0", countWakes GraphOptions.Default 0.0 -0.0, countWakes structural 0.0 -0.0 ]
for name, js, st in equalityRows do
printfn "%-20s default wakes: %d, structural wakes: %d" name js st
equal record default wakes: 1, structural wakes: 0
Some 1 over Some 1 default wakes: 1, structural wakes: 0
None over None default wakes: 0, structural wakes: 0
nan over nan default wakes: 1, structural wakes: 0
0.0 over -0.0 default wakes: 0, structural wakes: 0
Differences between .NET and Fable
The table below shows whether writing an equal but separately built value is cut off. Some value types, including DateTime and decimal, compile to objects under Fable, so the default comparison can differ between targets.
Writing the same object instance back is cut off on both targets.
| Type | Default on .NET | Default under Fable | StructuralPolicy, both targets |
|---|---|---|---|
int, float, string | Cut off | Cut off | Cut off |
nan | Propagates | Propagates | Cut off on .NET, propagates under Fable |
DateTime, DateTimeOffset, decimal | Cut off | Propagates | Cut off |
| Struct records, struct tuples | Cut off | Propagates | Cut off |
Some 1 | Propagates | Cut off | Cut off |
None | Cut off | Cut off | Cut off |
Records, tuples, lists, Some of a record | Propagates | Propagates | Cut off |
| Class without custom equality | Propagates | Propagates | Propagates |
Per-node comparers
Use a WithComparer factory to give one node an IEqualityComparer<'T>. Other nodes keep the graph policy. Call these factories inside an active graph, as with their ordinary counterparts.
let name = createSignalWithComparer System.StringComparer.OrdinalIgnoreCase "Ada"
let label =
createMemoWithComparer System.StringComparer.OrdinalIgnoreCase (fun _ -> name.Value.Trim())
name.Value <- "ADA"
printfn "%s" name.Value // Ada: an equal signal write keeps the stored value
A signal rejects an equal write. A memo caches its newly computed result but suppresses downstream work when it compares equal. The comparer controls settled-value cutoffs; pending, failed, waiting and caught-error state changes still propagate.
The opt-in factories are createSignalWithComparer, createMemoWithComparer, createOwningMemoWithComparer, createEffectOnWithComparer, createSuspenseWithComparer, createErrorBoundaryWithComparer and createBoundaryWithComparer. Their ownership, purity and previous-value rules match the ordinary factories. Comparers are selected once at construction. Async sources, async memos, stream memos and collection value cutoffs continue to use the graph policy.
Bindings use their source's cutoff. Give the signal or memo a comparer when a target should ignore equivalent values; the binding has no separate equality policy. Projection item equality also determines which key-selection work a pass can skip, while keys themselves always compare structurally; see Projection identity.
Invalid comparers and comparer failures
Null comparers are rejected at construction. If a signal's comparer throws, the signal stays unchanged and the exception reaches its writer. On a computed node, a comparer exception fails the node through its existing error handling. A boundary's recovery handler handles body errors; a comparer exception fails the boundary directly.
From C#, pass the comparer as an additional argument:
var name = Reactive.Signal("Ada", StringComparer.OrdinalIgnoreCase);
var label = Reactive.Memo(() => name.Value.Trim(), StringComparer.OrdinalIgnoreCase);
Graph-wide policies
Define a custom equality policy
Pass an IEqualityPolicy as GraphOptions.Equality. Its Comparer<'T> supplies the comparer for each value type and is called once when a node using the graph policy is created. A per-node comparer overrides it for that node.
For example, this graph treats strings that differ only in case as equal:
type CaseInsensitivePolicy() =
interface IEqualityPolicy with
member _.Comparer<'T>() =
{ new System.Collections.Generic.IEqualityComparer<'T> with
member _.Equals(a, b) =
System.String.Equals (string (box a), string (box b), System.StringComparison.OrdinalIgnoreCase)
member _.GetHashCode a = (string (box a)).ToLowerInvariant().GetHashCode () }
let caseInsensitiveWakes =
countWakes { GraphOptions.Default with Equality = CaseInsensitivePolicy () } "abc" "ABC"
printfn "case-insensitive wakes: %d" caseInsensitiveWakes
case-insensitive wakes: 0