# 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`.

:::details 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 |

:::

:::tip Write a new object to propagate a change
Under `JsIdentityPolicy`, mutating a referenced object in place and writing the same reference back
is cut off. Write a new value to propagate a change.
:::

:::details Compare the policies on .NET

This example counts the effect runs caused by each write, excluding the initial run.

```fsharp
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
```

```text
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
```

:::

:::details 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.

```fsharp
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](bindings.md) 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](projections.fsx#identity).

:::details 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:

```csharp
var name = Reactive.Signal("Ada", StringComparer.OrdinalIgnoreCase);
var label = Reactive.Memo(() => name.Value.Trim(), StringComparer.OrdinalIgnoreCase);
```

## Graph-wide policies

:::details 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:

```fsharp
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
```

```text
case-insensitive wakes: 0
```

:::
