Skip to content

Instantly share code, notes, and snippets.

@rjdestigter
Last active November 27, 2018 22:33
Show Gist options
  • Save rjdestigter/4802fb27eaef5e4e8795df4d151553e1 to your computer and use it in GitHub Desktop.
Save rjdestigter/4802fb27eaef5e4e8795df4d151553e1 to your computer and use it in GitHub Desktop.
Memoizing element transformations of array data selectors
// Redux
import * as _ from 'lodash'
import { createSelectorCreator } from 'reselect'
import memoize from './defaultMemoizeWithDeepEqualsOutput'
/** Selector creator for lists. Uses _.isEqual for deeper array comparisons */
export default createSelectorCreator(memoize as any, _.isEqual)
import * as _ from 'lodash'
import { defaultMemoize } from 'reselect'
const defaultMemoizeWithDeepEqualsOutput = <TS extends any[], R, MA extends any[]>(
func: (...args: TS) => R,
...memoizeOptions: MA
) => {
let lastResult: R | undefined
const memoized = defaultMemoize(func, ...memoizeOptions)
return (...args: TS) => {
const nextResult = memoized(...args)
if (_.isEqual(nextResult, lastResult)) {
return lastResult
}
lastResult = nextResult
return lastResult
}
}
export default defaultMemoizeWithDeepEqualsOutput
import { createSelector, defaultMemoize, Selector } from 'reselect'
import deepEqualOutputMemoize from './defaultMemoizeWithDeepEqualsOutput'
/**
* Map (fmap) from selector A to selector B.
*
* @param f Function that maps a -> b
* @param selector$ Selector that, given state S returns A
* @returns A selector that, given state S, returns B
*/
export const map$ = <S, A>(selector$: Selector<S, A>, selectorCreator = createSelector) => <B>(f: (a: A) => B) =>
selectorCreator(selector$, outputA => f(outputA))
/**
* Flat map a selector of a selector. Also known as "bind" (haskell) or "chain" (fp-ts)
* @param f Function that, given the result of selector A returns a selector of B
* @param a$ A selector of A
* @returns A function that, given state S, returns B
*/
export const flatMap$ = <S, A>(a$: Selector<S, A>, memoizeWithDeepEqualOutput = false) => <B>(
f: (a: A) => Selector<S, B>
): Selector<S, B> => {
let previousOutputOfA: A | undefined
let b$: Selector<S, B> | undefined
const memoize: any = memoizeWithDeepEqualOutput ? deepEqualOutputMemoize : defaultMemoize
return memoize((state: S) => {
const nextA = a$(state)
if (nextA !== previousOutputOfA || b$ == null) {
previousOutputOfA = nextA
b$ = f(previousOutputOfA)
}
return b$(state)
})
}
export const chain$ = flatMap$
import * as _ from 'lodash'
import { Selector } from 'reselect'
import createDeepEqualSelector from './createDeepEqualSelector'
import { map$ } from './fp'
/**
* Selector that caches the transformation of each element in list X. This can help
* make long lists of transformations or expensive-ish transformations more performant.
*
* If the output of the input-selector (`Selector<any, X[]>`) changes, than the
* selector returned by this function will rerun.
*
* It will iterate the list of Xs and for each element apply transformation `f` to it unless
* the result of the transformation already exists in the `Map<X, Y>`
*
* The `cleanUp` function wil remove any keys X from `Map<K, Y>` if they do not exist anymore
* in the ouput of the input-selector. This clean up is pushed to the bottom of the stack using
* `setTimeout`
*
* It's also using `createDeepEqualSelector` as this not only checks if the input to Y$ has changed
* but also deep equal checks if it's output is the same as before.
*
* @param selector$ X selector returning a list of Xs
* @param getKey Function that takes X and returns the key used to store references in the map. Defaults to _.identity
* @returns X selector returning a list of Ys
*/
export default <X, K>(selector$: Selector<any, X[]>, getKey: (x: X) => K = _.identity) => <Y>(
f: (a: X) => Y
): Selector<any, Y[]> => {
// X map of X -> Y
const lookupMap: Map<K, Y> = new Map()
const cleanUp = (xs: X[]) =>
setTimeout(() => {
const keys = Array.from(lookupMap.keys())
const toBeRemoved = _.difference(keys, xs.map(getKey))
if (toBeRemoved.length > 0) {
toBeRemoved.forEach(x => lookupMap.delete(x))
}
}, 0)
return map$(selector$, createDeepEqualSelector)((xs: X[]) => {
const ys = xs.map(x => {
const fromLookupMap = lookupMap.get(getKey(x))
if (fromLookupMap) {
return fromLookupMap
}
const y = f(x)
lookupMap.set(getKey(x), y)
return y
})
cleanUp(xs)
return ys
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment