-
-
Save dtinth/e4329efd3882f926a134cb8a746d57a8 to your computer and use it in GitHub Desktop.
const memoizeArraySelector = (baseSelector) => { | |
let _last = [ ] | |
return (state) => { | |
const result = baseSelector(state) | |
if (arrayEquals(result, _last)) return _last | |
_last = result | |
return result | |
} | |
} | |
export const mapSelector = f => baseSelector => createSelector(baseSelector, f) | |
export const mapSelectorForArray = f => baseSelector => memoizeArraySelector(mapSelector(f)(baseSelector)) | |
export const filteredBy = predicate => mapSelectorForArray(data => _.filter(data, predicate)) | |
export const rejectedBy = predicate => mapSelectorForArray(data => _.reject(data, predicate)) | |
export const sortedBy = sortBy => mapSelectorForArray(data => _.sortBy(data, sortBy)) | |
export const sortedByOrder = (sortByOrder, direction) => mapSelectorForArray(data => _.orderBy(data, sortByOrder, direction)) |
@dtinth not sure if this is practical working with team environment introducing Haskell-style type definition for documentation purpose. I guess it depends and feels like it quite interesting.
To think even further. Can we have this Haskell-ish type def as type checking tool like what flow does?
// "It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures."
// —Alan Perlis
// So, would it be better if we just memoize everything, no?
// Apart from composability point of view, it also simplifies the function name.
// Or is there any performance/space concern?
// memoizeSelector :: Selector s a -> Selector s a
const memoizeSelector = (baseSelector) => {
// memoize array only for now...
let _last = [ ]
return (state) => {
const result = baseSelector(state)
if (_.isArray(result) && arrayEquals(result, _last)) return _last
_last = result
return result
}
}
// smap :: (a -> b) -> Selector s a -> Selector s b
export const smap = f => baseSelector => memoizeSelector(createSelector(baseSelector, f))
// filterBy :: (a -> Bool) -> Selector s a[] -> Selector s a[]
export const filterBy = predicate => smap(data => _.filter(data, predicate))
// rejectBy :: (a -> Bool) -> Selector s a[] -> Selector s a[]
export const rejectBy = predicate => smap(data => _.reject(data, predicate))
// sortBy :: (a -> Bool) -> Selector s a[] -> Selector s a[]
export const sortBy = sortBy => smap(data => _.sortBy(data, sortBy))
// sortByOrder :: Object a => (Keys a, Direction[]) -> Selector s a[] -> Selector s a[]
export const sortByOrder = (order, direction) => smap(data => _.orderBy(data, sortByOrder, direction))
By the way, I really like your idea having selector as a Functor. I have never thought about it before.
(actually, to think about it, this is not just a functor but can make it a Reader monad isn't it?)
And if we are able to use pipeline operator |>
it would be even cooler.
// completedTasksSortedByTitle :: Selector GlobalState Task[]
const completedTasksSortedByTitle =
tasks
|> filterBy(task => task.completed)
|> sortBy(task => task.title)
-
About flow type, yep I agreed that Flow/TypeScript type notation makes it look very scary.
-
About making
memoizeSelector
work with both array and non-array, I like it a lot. -
Yes, it can be a Reader monad. For example, using
createSelector
to combine selectors can be considered a monadic operation:// Assume a selector is a fantasy-land monad instead of a (s => a) function. const combine2 = s1 => s2 => f => s1.chain(v1 => s2.map(v2 => f(v1, v2)))
@dtinth I tried adding flow type to it but it's not pretty at all, it looks even scarier. Haskell-style type definition is much better to convey the meaning but not available in js :(