Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

framework: add treeSelect (a new type of memoized selector) #20547

Merged
merged 9 commits into from
Jan 15, 2018

Conversation

samouri
Copy link
Contributor

@samouri samouri commented Dec 6, 2017

summary

Selectors are a vital part of any redux app. We have a powerful tool for creating cached selectors already called createSelector. createSelector though has a fatal flaw which reselect also has. Namely that it only stores a single dependent-->cache mapping at a time. This means that a too-common case will have disastrous cache hit rates and hurt performance.

reselect solves this by creating a cached selector per react component instance.
if we wanted that to translate to createSelector We would need to make a createSelectorCreator or change the current implementation of createSelector.

I've already created a PR trying to modify createSelector, but we decided that be too dangerous because so much of Calypso depends on it. Instead I've opted to make a new function in this PR with a slightly altered API that I think will help developers fall into the pit of success.

proposed api

treeSelect( getDependents, selector ):

  • getDependents: A function which maps over state and returns all of the relevant parts of state the selector needs. You should be creating an array whose elements are all the return of other selectors -- no computations allowed here. Returned dependents values must also all be objects because in the implementation they act as keys into a WeakMap.
  • selector: A function which takes in the same args as getDependents with one catch. Instead of being passed state as its first arg, it is given the results of getDependents. This forces you to declare all of your state-dependencies.

example

const getDependents = state => [ posts: state.posts ];
const selector = ( [ posts ], siteID ) => filter( posts, { siteId } );

const getSitePosts = treeSelect( selector, getDependents)

what is good about this approach?

  1. By restricting the selector function so that it doesn't have access to state, it forces a selector to think critically about what it depends on. It also ensures that we only do the work of retrieving dependents once instead of twice like was usually done in createSelector.
  2. By using a WeakMap, we can garbage collect as soon as a state subtrees disappear.
  3. By using a dependency tree, we can efficiently cache on very granular dependencies with an O(dependencies.length) retrieval time.

References

@samouri samouri requested a review from spen December 6, 2017 02:15
@matticbot
Copy link
Contributor

@samouri samouri force-pushed the add/framework/create-cached-selector branch from af1584f to ad595cf Compare December 6, 2017 02:17
@samouri samouri changed the title Add/framework/create cached selector add createCachedSelector Dec 6, 2017
@samouri samouri changed the title add createCachedSelector framework: add createCachedSelector Dec 6, 2017
@samouri samouri force-pushed the add/framework/create-cached-selector branch from b032177 to 412d8d1 Compare December 6, 2017 03:05
@samouri samouri self-assigned this Dec 6, 2017
@samouri samouri changed the title framework: add createCachedSelector framework: add createCachedSelector (treeselect) Dec 6, 2017
@samouri samouri changed the title framework: add createCachedSelector (treeselect) framework: add createCachedSelector (treeSelect) Dec 6, 2017
@samouri samouri requested a review from jsnajdr December 6, 2017 03:21
@designsimply designsimply added Framework [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Dec 11, 2017
@samouri
Copy link
Contributor Author

samouri commented Dec 11, 2017

should the the dependency map convert into a map when a primitive value is added (LazyWeakMap) that way a selector may depend on a non-object value that is in state?

Update: added support for non-object keys via MixedMap. Its possible we could slightly optimize for specific situations in which getDependents returns a smattering of objects and keys by sorting the dependents so that objects are first when using them as keys to the cache. I don't think it'd be helpful enough to warrant implementing though.

constructor( init ) {
forEach( init, ( [ key, val ] ) => {
this.mapForKey( key ).set( key, val );
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you see this on MDN

var kvArray = [['key1', 'value1'], ['key2', 'value2']];

// Use the regular Map constructor to transform a 2D key-value Array into a map
var myMap = new Map(kvArray);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep! which is why i needed to bake in some custom support for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this constructor takes all the values that have object keys and throws them into the weakmap. the rest go into the regular map

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah right. good good


delete( k ) {
this.mapForKey( k ).delete( k );
return this;
Copy link
Contributor

@dmsnell dmsnell Dec 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you anticipate chaining these calls like cache.clear().set().set() etc…?

I find that it's helpful to not return this when we are mutating the underlying object because the code itself kind of indicates based on return value if this is a side-effecting action or a pure one. moment() is a good example where it supports chaining but the underlying object mutates throughout the chain. it gives the appearance of being safe from a purity standpoint, but that can lead to mistakes on that assumption.

const then = moment( thenDate )
const nextDay = then.startOf( 'day' ).add( 1, 'day' )
const prevDay = then.startOf( 'day' ).subtract( 1, 'day' )

^^^ looks fine until you realize that prevDay is actually the start of thenDate

const then = moment( thenDate )
const nextDay = then

nextDay.startOf( 'day' ).add( 1, 'day' )

^^^ wait, that's odd, something must be up - I wonder if there's implicit mutation happening…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you anticipate chaining these calls like cache.clear().set().set() etc…?

I don't think returning this indicates purity...I don't usually assume purity unless its specifically stated.

The thing I would want most is to adhere to the WeakMap specification. Some of them return this and some don't

  • set --> returns this
  • get --> returns the value associated with the key
  • clear --> returns undefined
  • delete --> returns true/false depending on if the key was found and removed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

odd. carry on.

import warn from 'lib/warn';

/*
* A map that is Weak with objects but Strong with primitives
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a great description

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😊

Copy link
Contributor

@blowery blowery Dec 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have this? It strikes me as a dangerous idea, as anything that ends up in the strong map is effectively a memory leak. The point of using a WeakMap is to weakly hold the keys so that when they go out of scope elsewhere, we can GC them. If we allow some keys to end up in the strong map, they're never be GC'd.

For true primitives, that's probably ok, they don't take up much space, but then there's also not much benefit to caching them this way. For strings, it could get ugly, as they're effectively unbounded in size.

So why do we need this?

Copy link
Contributor Author

@samouri samouri Dec 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was torn on whether or not to allow primitives.
There are perfectly valid use-cases for having a primitive as a dependent.
Imagine state looked like this:

state = {
  priceOfSite: 5.40,
  sites: [ ... ],
  ...
}

If primitives weren't allowed, then you wouldn't be able to write a cached selector that used priceOfSite. For example:

// would be invalid without MixedMap
const getTotalPrice = createCachedSelector( 
  ({ sites, price }) =>return price * sites.length,
  state => ({ sites: state.sites, price: state.priceOfSite }) 
)

The solutions i could think of are:

  1. restrict this cachedSelector function to to selectors that depend on objects within state, no primitives allowed.
  2. MixedMap
  3. selector-ception: Do something clever with primitives. If we notice that a dependent is a primitive, then create a cached function for it so that it will return the value wrapped in an object { value: 'primitive-value', objectified: true } and make sure to return a referentially equal one whenever the value hasn't changed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blowery we can't use primitive values as keys in WeakMaps sadly. that is, this is getting as much out of the WeakMap as we can and only falling back to classical maps when JavaScript won't let us use the WeakMap

our alternative is probably to skip caching these values

* @return {Function} Cached selector
*/
export default function createCachedSelector( { selector, getDependents } ) {
if ( ! isFunction( selector ) || ! isFunction( getDependents ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cough TypeScript

Copy link
Contributor Author

@samouri samouri Dec 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export default function createCachedSelector( selector: Function, getDependents:  Function )

would be nice :)


const cache = new MixedMap();

const cachedSelector = function( state, ...args ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function( { state, ...args } )

:trollface:

const cachedSelector = function( state, ...args ) {
const dependents = getDependents( state, ...args );
const sortedDependentsArray = Object.keys( dependents )
.sort()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not what you think it is, it is also not stable

Copy link
Contributor Author

@samouri samouri Dec 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we discussed this on slack. the keys will always be unique strings therefore Array#sort does what we want -- to produce the same array for the same object of dependents every time

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enumeration order of object properties is guaranteed to be the same as the order in which the properties were created. So you don't need to sort the keys nearly as often as you might think. You still need to make cases like this behave correctly:

getDependents = state => cond ? { dep1, dep2 } : { dep2, dep1 };

The fact that you require a particular property order, and that the keys are not used by the createCachedSelector for anything, suggests that the data structure you're really looking for is an array, not an object:

createCachedSelector(
  ( [ dep1, dep2 ], ...args ) => combine( dep1, dep2, args ),
  ( state, ...args ) => [ dep1, dep2 ]
);

Or, even better in my opinion, follow the widely used reselect function signature:

createCachedSelector(
  ( state, ...args ) => computeDep1,
  ( state, ...args ) => computeDep2,
  ( dep1, dep2, ...args ) => combine( dep1, dep2, args )
)

The disadvantage here is that it's hard to add a getKey function as another argument, if you ever needed it. And the code in #21507 suggests that you might need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that you require a particular property order, and that the keys are not used by the createCachedSelector for anything, suggests that the data structure you're really looking for is an array, not an object:

They keys are used! Not in a way that is necessary, but in a way that I think makes for a nice api. The return value of getDependents becomes the first parameter to selector

So for example:

getDependents = (state, siteId) => ({ site: getSite(siteId), posts: getPosts(siteId), comments: getComments(siteId)});
selector = ({site, posts, comments}) => doThings(site, posts, comments)

All that said, I may be alone in how much I love using objects for named parameters, so I'll switch to an array that may make more sense here for all the reasons you cited:

getDependents = (state, siteId) => [getSite(siteId), getPosts(siteId), getComments(siteId)];
selector = ([site, posts, comments]) => doThings(site, posts, comments)

While the reselect api is really pleasant, I agree that we may need to sometimes use custom cache key generation which discounts that as an option. If we were rebuilding calypso from scratch we wouldn't, but in order to easily transition code that already depends on objects as selector args then we'll need it.


if ( process.env.NODE_ENV !== 'production' ) {
if ( some( args, isObject ) ) {
warn( 'Do not pass complex objects as arguments to a cachedSelector' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this important? we have a number of existing selectors which take objects as arguments after state

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's part of the current createSelector as well.

If an argument is an object literal then it shouldn't be used for caching because it's a new object on each call. if it is in object from state, then it should be part of getDependents

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it's a new object on each call

assumption

still, it may be part of createSelector, but what I understand is that we're saying, "if you pass any object into a selector we won't let you cache things" when it seems like it could be valid

like what if the dependents are based on a property in that object, or unrelated to them at all?

I think this might occur when we pass things like formatting info to the selectors and stuff like that where the last step in the selector is to do some specific twiddling based on the passed option bag

Copy link
Contributor Author

@samouri samouri Dec 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assumption

object literals are by definition new every time used (like the options bag you suggested).


The reason we can't use them is because there is no standard way to create a unique key for objects. for an ordered list of primitives its simple (args.join()).

If this ends up being a need then we can allow for custom cacheKey generation...but i'd rather stay away for this first iteration because YAGNI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can drop the "complex" part as even the empty object qualifies here. Maybe "You can only pass primitives as arguments to a cached selector"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropped the word complex in the latest update.

let currCache = cache;
forEach( sortedDependentsArray, dependent => {
if ( ! currCache.has( dependent ) ) {
currCache.set( dependent, new MixedMap() );
Copy link
Contributor

@dmsnell dmsnell Dec 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line may mutate cache but it doesn't look like it from here. this is confusing to me and relates to my comment above in MixedMap

I think what's happening is that we are using currCache more as a cursor than as an object and we're moving that cursor through cache. if this is happening, then I think we can rearrange the code to more explicitly reflect that reality

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love input on how to do that! I have no idea 🤔

I like the word cursor for what I'm doing with currCache, its more descriptive

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using cursor as the name would help I think.

this is really a kind of insertion on a prefix tree where the dependents are the atoms forming the nodes. things could potentially be more clear simply with the proper messaging around them…

const upsert = ( cache, value ) => {
	if ( ! cache.has( value ) ) {
		cache.set( value, new MixedMap() )
	}

	return cache.get( value );
}

// insert dependents into cache tree
sortedDependentsArray.reduce( upsert, cache )

maybe even something along these lines…

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samouri did you see this last comment as it pertains to the naming of the cursor? (it eliminates it, for example)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I do! I think its brilliant! I'm just changing the name to insertIfAbsent but otherwise keeping the logic intact.

jest.mock( 'lib/warn', () => jest.fn() );

describe( 'index', () => {
describe( '#createCachedSelector', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the root of this file will already appear as create-cached-selector so this two-level descent may report in the tests as create-cached-selector/index/#createCachedSelector

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a couple of errors to show how it outputs:
screen shot 2017-12-19 at 8 04 02 pm

What do you think?

getSitePosts( reduxState, 2916284 );

expect( selector.mock.calls.length ).toBe( 1 );
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good partner-test here might be one to verify that this gets called twice when the arguments are different. This test has a high risk of false positives on the behavior of the function.

test( 'should call selector when making non-cached calls', () => {
	getSitePosts( reduxState, 2916284 )
	getSitePosts( reduxState, 2916284 + 1 )

	expect( selector.mock.calls.length ).toBe( 2 );
} )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current test named that should return the expected value of differing arguments does this.

I like your test naming better though, I'll switch

getSitePosts( state, 1, [] );

expect( warn.mock.calls.length ).toBe( 3 );
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as per my earlier comment, I question the validity of this constraint

@samouri
Copy link
Contributor Author

samouri commented Jan 11, 2018

I just tested out the optimization gains that can be had by using this selector and they were stark.
The test is here: #21468

createSelector treeSelect
wasted renders 351 8

cc @blowery re. conversations perf boost

const getDependents = state => ( { posts: state.posts } );
const selector = ( { posts }, siteId ) => filter( posts, { siteId } );

const getSitePosts = createCachedSelector( selector, getDependents );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't it make more sense to pass getDependents first since they are logically prior to the selector?

this isn't a big deal either way, but something feels odd about this ordering

Copy link
Contributor Author

@samouri samouri Jan 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you wholeheartedly. I only picked this order to match createSelector's api.
Since we are diverging anyway, may as well diverge a bit more ;).

@dmsnell
Copy link
Contributor

dmsnell commented Jan 14, 2018

@samouri what do you need to get this out the door? I think getting it in soon will be a big boon for us

@samouri samouri changed the title framework: add createCachedSelector (treeSelect) framework: add treeSelect (a new type of memoized selector) Jan 15, 2018
@samouri
Copy link
Contributor Author

samouri commented Jan 15, 2018

@samouri what do you need to get this out the door? I think getting it in soon will be a big boon for us

The main question I'd been pondering was how to handle primitives. For now I think its safest to disallow them completely. We may realize that we need to allow primitives at some point, but until then I'd rather explicitly not support it. I also have a gut feeling that we'll be able to work around the restriction by having selectors grab items higher up in state instead.


So the changes i've made based on feedback are:

  1. Swapped the arguments order (@dmsnell)
  2. Made getDependents return an array instead of an object (@jsnajdr)
  3. Disallow primitives as dependents, and delete MixedMap (@blowery).
  4. Renamed from createCachedSelector --> treeSelect

Two things that could happen in in future prs is allowing for custom cacheKeys and re-enabling primitives.

cursor.set( key, selector( dependents, ...args ) );
}

return cursor.get( key );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you already know the return value (it's the one that you just inserted into the map), so you can avoid the extra map lookup here.

Selectors are called zillion times on every possible Redux dispatch, so even a tiny optimization like this is very likely to be detectable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'll require an extra line to assign the value to a variable. I'll do that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more line for Jake, one less function call for the CPU. Looks like a good tradeoff 😜

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe. we probably shouldn't make too many assumptions about this though due to JS engine optimization. probably less CPU work but not definitely. maybe not significant but probably not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmsnell I'm pretty sure that the call to Map.prototype.get would be actually executed by the engine and that it takes more than zero CPU cycles ;)

*/
function insertDependentKey( map, key, currentIndex, arr ) {
const NewMap = currentIndex === arr.length - 1 ? Map : WeakMap;
return insertIfAbsent( map, key, new NewMap() );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create the new map only when you're really going to need it. Most of them will be thrown away.

Copy link
Contributor Author

@samouri samouri Jan 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats a good point 👍

}

map.set( key, value );
return map.get( key );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just return the value without an extra map lookup.

* @return {Function} Cached selector
*/
export default function treeSelect( getDependents, selector ) {
if ( ! isFunction( getDependents ) || ! isFunction( selector ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check could be done only in development mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, adding check

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* Note: Inserts WeakMaps except for the last map which will be a regular Map.
*/
function insertDependentKey( map, key, currentIndex, arr ) {
const NewMap = currentIndex === arr.length - 1 ? Map : WeakMap;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured out this function only after stepping through it in debugger. I was wondering why there is a classic Map for the last dependent. Its results are also objects, aren't they? And then I understood that it's all offset by one and the last Map is for string keys coming from ...args :) Some explanatory comment would be helpful here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nailed it! I'll add in a comment explaining that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I adjusted the comment slightly. I'm having trouble coming up with a good explanation so feel free to rewrite it (or post here a better one and I'll modify it)

# `treeSelect`

This module exports a function which creates a cached state selector for use with the Redux global application state. It is a good idea to use this function over plain selectors whenever either the computation over state or React's rerenders are expensive.
It is called `treeSelect` because it internally uses a [WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) based dependency tree in order to allow the gc to free memory without explicitly clearing the cache.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is called treeSelect because it internally uses a [WeakMap]

That doesn't make sense. I think we're missing the logical warrant or we probably just need to remove the reference to WeakMap or add it later when it's not tied to tree

Copy link
Contributor Author

@samouri samouri Jan 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read a few more words and then I think it makes sense:

It is called treeSelect because it internally uses a WeakMap based dependency tree.

I can switch around the wording so that the word tree is emphasized more though. Something like:

it is called treeSelect because it internally uses a tree of dependencies in order to allow the gc to free memory without explicitly clearing the cache.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read a few more words and then I think it makes sense:

I read those words and the sense never came

Something like:

Removing the WeakMap mention as you did makes sense.

For example, imagine that our state contains post objects, each of which are assigned to a particular site. Retrieving an array of posts for a specific site would require us to filter over all of the known posts. While this would normally trigger a re-render in React, we can use `treeSelect` to create a cached version of the selector which can return a referentially equal object:

```js
const getDependents = state => ( { posts: state.posts } );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while I think dependent is more proper I find myself constantly making typos in createSelector code because it uses dependant with an a

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️


```js
const getDependents = state => ( { posts: state.posts } );
const selector = ( { posts }, siteId ) => filter( posts, { siteId } );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be updated to an array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it does!

return value;
};

return cachedSelector;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why store it as a separate binding vs just returning the function expression?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the nittiest of nits :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no reason any more. beforehand it was so that we could add properties to cachedSelector with cachedSelector.cache = .... Since its not necessary anymore I can get rid of it

// create a dependency tree for caching selector results.
// this is beneficial over standard memoization techniques so that we can
// garbage collect any values that are based on outdated dependents
const cursor = dependents.reduce( insertDependentKey, cache );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at this point I don't see much value in the cursor name because we're not "cursing" around anymore.

what about just calling it something like leafCache or leaf or callCache or something like that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like leafCache

// garbage collect any values that are based on outdated dependents
const cursor = dependents.reduce( insertDependentKey, cache );

const key = args.join();
Copy link
Contributor

@dmsnell dmsnell Jan 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is something I think is still dubious. it seems like there are reasons why passing objects makes sense (like a moment time value or some kind of query object). I guess I'm fine leaving it as-is as long as we are open to adjusting as developers hit the stringable-data-type wall

this is something that getCacheKey() can actually work around in createSelector

we're specifying a kind of relational division operation to collapse "unique" invocations

Copy link
Contributor Author

@samouri samouri Jan 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already called out that we may want to add it in a future pr:

Two things that could happen in in future prs is allowing for custom cacheKeys and re-enabling primitives.

for now, less is more

Copy link
Contributor

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Copy link
Member

@jsnajdr jsnajdr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Thanks for working on this 🚀

*/
function insertDependentKey( map, key, currentIndex, arr ) {
if ( map.has( key ) ) {
return map.get( key );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could shave off another lookup by only calling map.get and checking for a falsy result.

@samouri samouri merged commit 8826135 into master Jan 15, 2018
@samouri samouri deleted the add/framework/create-cached-selector branch January 16, 2018 03:31
@sirreal sirreal removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Mar 27, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants