politie / sherlock

A reactive programming library for JavaScript applications, built with TypeScript.
Apache License 2.0
39 stars 12 forks source link

Pluggable equals function #14

Closed pebell closed 6 years ago

pebell commented 6 years ago

The documentation on the Atom class states:

Setting the state to an immutable object that is structurally equal to the previous immutable object is not considered a state change.

The current implementation uses the equals method (if it exists) on the atom value to determine if a state change has occurred. This works out of the box with Facebooks Immutable.js because these objects expose an equals method. But ordinary objects or objects created with for example seamless-immutable do not provide such an equals method and are therefore less efficient to use with Sherlock.

By allowing to initialize an Atom with a custom or "pluggable" equals function, one could create an Atom that works with all kind of data structures, not only objects with an equals method.

pavadeli commented 6 years ago

A very good idea, and should be part of our roadmap.

Equality functions are not only used for Atoms, but also for derivations and reactors. When a derivation is recalculated, the new value should be compared to the old value using the "pluggable" equals function. This is part of the algorithm that ensures that the absolute minimum of work is done while at the same time ensuring reactors that need to fire are fired.

I see several solutions that we need to investigate and perhaps implement multiple versions.

  1. Provide a way to configure a single atom or derivable during creation. This could look as follows:

    import { atom, derivation } from '@politie/sherlock';
    
    // Normal use:
    const a$ = atom('some value');
    const d$ = derivation(() => a$.get() + '!');
    
    // Use a custom equals function that only compares lengths:
    function myEquals(a: string, b: string) { return a.length === b.length; }
    
    const b$ = atom({ equals: myEquals }, 'some other value');
    const d$ = derivation({ equals: myEquals }, () => a$.get() + b$.get());
  2. Provide a way to transform an atom or derivable with one equals function into an atom or derivable with another equals function as follows:

    import { atom, derivation } from '@politie/sherlock';
    
    // Normal use:
    const a$ = atom('some value');
    
    // Use a custom equals function that only compares lengths:
    const b$ = a$.withEquals(myEquals);
    
    // Or when used in a derivation:
    const d$ = derivation(() => a$.get() + b$.get()).withEquals(myEquals);
  3. Provide a way to configure a new "instance" of the entire Sherlock library where the equals function is configured (and perhaps in the future other configs as well). This could work something like this:

    // my-sherlock.ts
    // A local file that configures Sherlock just the way you want it for your own
    // project. In the rest of the project import all Sherlock primitives from here.
    
    // Export all symbols from sherlock.
    export * from '@politie/sherlock'
    
    // But overwrite atom, lens and derivation to use our equals function.
    import { createInstance } from '@politie/sherlock'
    
    const { atom, derivation, lens } = createInstance({ equals: myEquals });
    export { atom, derivation, lens };
  4. Simply use a config object that configures Sherlock globally. This is what most other libraries do.

Let me know what you think.