Skip to content

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 ===.
  • StructuralPolicy uses EqualityComparer<'T>.Default.
How the policies compare values on .NET
Value kindJsIdentityPolicy (default)StructuralPolicy
Primitives, strings, structs, struct tuplesBy valueBy value
Records, unions, Some x, other reference typesBy reference: an equal copy propagatesBy value: an equal copy is cut off
None over NoneCut off (None is null)Cut off
nan over nanPropagates (IEEE: nan equals nothing)Cut off
0.0 over -0.0Cut offCut 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.

TypeDefault on .NETDefault under FableStructuralPolicy, both targets
int, float, stringCut offCut offCut off
nanPropagatesPropagatesCut off on .NET, propagates under Fable
DateTime, DateTimeOffset, decimalCut offPropagatesCut off
Struct records, struct tuplesCut offPropagatesCut off
Some 1PropagatesCut offCut off
NoneCut offCut offCut off
Records, tuples, lists, Some of a recordPropagatesPropagatesCut off
Class without custom equalityPropagatesPropagatesPropagates

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
Edit this page