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

@cached #566

Merged
merged 5 commits into from
Feb 26, 2021
Merged

@cached #566

merged 5 commits into from
Feb 26, 2021

Conversation

pzuraq
Copy link
Contributor

@pzuraq pzuraq commented Dec 23, 2019

@pzuraq pzuraq changed the title Adds @memo RFC @memo Dec 23, 2019
not change. Unfortunately, this is not really something that `@memo` can
circumvent, since there's no way to tell if a value has actually changed, or
to know which values are being accessed when the memoized value is accessed
until the getter is run.
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 bit confusing. If the purpose of @memo is to not re-reun the getter unless the underlying properties have changed, how can the statement "may rerun even if the values themselves have not changed" be true? It seems that it's implying that any set operation invalidates the @memoized value, but I don't see that stated elsewhere in this RFC. Maybe it would be good to add that if it's true?

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, and that was specified in the original tracked properties RFC: https://github.com/emberjs/rfcs/blob/master/text/0410-tracked-properties.md#manual-invalidation

This clause basically is stating that @memo cannot possibly know whether a value has actually changed. All it can know is if the tags have changed, and it will defer to the change semantics that were defined upstream. In the future, we could change @tracked to check if the value is different potentially, and not invalidate unless it was. We could also add a new decorator which does this.

Copy link
Contributor

Choose a reason for hiding this comment

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

In the future, we could change @tracked to check if the value is different potentially, and not invalidate unless it was. We could also add a new decorator which does this.

This would be very interesting to see!

Copy link
Contributor

@mehulkar mehulkar Dec 23, 2019

Choose a reason for hiding this comment

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

Yes, and that was specified in the original tracked properties RFC:

Hmm, so the RFC for "tracked" may state how invalidation works, but to me, the whole purpose of a @memo decorator is to change how invalidation works. That's why I felt it would be appropriate to say more here about what causes a value to be returned from the cache and what causes the getter to re-run.

Maybe a code example would help:

class Foo {
  @tracked bar;
  @memo
  get modBar() {
    return this.bar + 1;
  }
}

const foo = new Foo();
foo.bar = 1;
console.log(foo.modBar);
foo.bar = 1;
console.log(foo.modBar);

With this code, the modBar getter will re-evaluate both times it is accessed even though foo has not changed value. (laying it out explicitly since I'm still only 95% sure that that's what the RFC is trying to say)

@mehulkar
Copy link
Contributor

Looks great! Couple things:

  • Is it possible to detail how this decorator could be debugged if a developer wants to understand how an invalidation occurred? (In other words, which tracked property was invalidated)
  • I know this is a slipper slope, but does it make sense for events to be dispatched on an invalidation? This would kinda bring back the observer model though, so I'm not sure it's a good idea.

@pzuraq
Copy link
Contributor Author

pzuraq commented Dec 23, 2019

Is it possible to detail how this decorator could be debugged if a developer wants to understand how an invalidation occurred? (In other words, which tracked property was invalidated)

This is a bit of an orthogonal concern, and not really in the scope of this RFC I think. We are definitely working on better debug tooling here though, I think a future RFC is definitely in order!

I know this is a slipper slope, but does it make sense for events to be dispatched on an invalidation? This would kinda bring back the observer model though, so I'm not sure it's a good idea.

This is not possible with the autotracking system, since invalidation is only observable later on when the value is consumed. So, it would basically be sending the event the first time (if ever) that the memoized value was read.

Fundamentally, this is just not something that lazy, pulled based change tracking can accomplish.

Copy link
Contributor

@snewcomer snewcomer left a comment

Choose a reason for hiding this comment

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

This is a great RFC and will be super beneficial to the community!

}
```

In this example, the `fullName` getter will be memoized whenever it is called,
Copy link
Contributor

@snewcomer snewcomer Dec 23, 2019

Choose a reason for hiding this comment

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

the fullName getter return value will be memoized

Although perhaps both say the same thing.

Cycles will not be allowed with `@memo`. The cache will only be activated
_after_ the getter has fully calculated, so any cycles will cause infinite
recursion (and eventually, stack overflow), just like un-memoized getters. If a
cycle is detected in `DEBUG` mode, it will throw an error.
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 good information!

A few other questions I had. It seems this is a per instance memoization technique? Will this only cache the last result (for memory safety)? Do you think these are relevant points or just implementation details that don't need to be flushed out at this time?

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 definitely would be per-instance, and it would capture only the last result. I think we can specify it here 👍

- Adds extra complexity when programming (whether or not a value should be
memoized is now a decision that has to be made). In general, we should make
sure this is not an issue by recommending that memoization idiomatically _not_
be used _unless_ it is absolutely necessary.
Copy link
Contributor

Choose a reason for hiding this comment

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

A few spots you discussed potential downside but I'm not sure I grasped them simply from reading. Is it cache space, performance, complexity? (and sorry if I didn't read carefully enough)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Every @memod property means.

  1. Slightly more startup cost, to decorate the property
  2. Slightly more memory cost, to cache the values
  3. Slightly more runtime cost to calculate the value, since it is wrapped in another function now
  4. Slightly more cost to revalidate on every render, even if nothing has changed.

These costs are tiny, but if every getter in an app was decorated, they would add up.

Copy link
Contributor

@buschtoens buschtoens Dec 23, 2019

Choose a reason for hiding this comment

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

Can you add this information to the How we teach this section, so it doesn't get lost accidentally? I think this is vital to understanding when to @memo or not to @memo.

Maybe the advanced guides could also go into detail about how to performance profile this. Can we make this info easily accessible in the Ember Inspector maybe?

and will only be recalculated the next time the `firstName` or `lastName`
properties are set. This would apply to any autotracking tags consumed while
calculating the getter, so changes to `EmberArray`s and other tracked
primitives, for instance, would also cause invalidations.
Copy link
Contributor

@snewcomer snewcomer Dec 23, 2019

Choose a reason for hiding this comment

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

@tracked arr = [1, 2, 3];

Perhaps it was only not clear to me but would changing arr to [1,3,4] also invalidate the cache (using your tracked-built-ins)? Or only if a new arr is set?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That’s exactly what that was meant to capture. Essentially, any autotrack-able change should bust the cache. If it would invalidate the template, it would invalidate @memo.

@yarigpopov
Copy link

I like ‘@cached’ better.

assigning the property:

```js
if (newValue !== this.trackedProp) {
Copy link
Sponsor Contributor

Choose a reason for hiding this comment

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

I especially like this requirement, because equality gets fuzzy when you start working with non-primitives.

In the documentation for this, I think it'd be good to include examples of complex equality checking through layer of tracked objects to demonstrate that it would not be possible to performantly check value diffs in any memorization implementation?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also like that this is laid out in the RFC, but I think it should be part of the the How We Teach This section also. In my previous experience (with Ruby), a memoized value does not change unless the cache itself is reset. Whereas in this case, it's unexpectedly(?) resetting when an upstream value is set. I wonder of either I understand memoization incorrectly or that it really can mean different things? Here's an example of how I've memoized in Ruby:

class Foo
  def bar
    @_bar || some_expensive_method
  end

  private

  def some_expensive_method
    # some expensive code
  end
end

the memoized return value of Foo#bar doesn't depend on anything else other than the cached instance variable @_bar.

@NullVoxPopuli
Copy link
Sponsor Contributor

I like this a lot.
I def think memo is the right word for this.
Especially if It's explicit about the tags/references that are checked. Maybe this ends up having people learn about references... But... That's fine. :)

Copy link
Contributor

@buschtoens buschtoens left a comment

Choose a reason for hiding this comment

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

Awesome!

`@memo` is not an essential part of the reactivity model, so it shouldn't be
covered during the main component/reactivity guide flow. Instead, it should be
covered in the intermediate/in-depth guides, and in any performance related
guides.
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a cross-link in parentheses should still be added? A question that I would immediately ask myself when reading the explanation for regular getters and @tracked: "Isn't this super inefficient?"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Really? I'm going to be doing some talks about it soon, but yeah the strategy is actually quite efficient, at least compared to similar frameworks like MobX and Vue's data stuff, where they all use observables/streams under the hood.

I think we should probably add a section once we get public autotracking primitives (which hopefully will be sooner rather than later)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I guess it's because I'd be coming in with a pre-Octane mindset, where everything was memoized by default and it just "felt right", because you seem to be saving unnecessary work. The fact that the actual memoization itself also takes time is hidden away and not immediately apparent to the user.

I'm super thrilled to see these talks!

- Adds extra complexity when programming (whether or not a value should be
memoized is now a decision that has to be made). In general, we should make
sure this is not an issue by recommending that memoization idiomatically _not_
be used _unless_ it is absolutely necessary.
Copy link
Contributor

@buschtoens buschtoens Dec 23, 2019

Choose a reason for hiding this comment

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

Can you add this information to the How we teach this section, so it doesn't get lost accidentally? I think this is vital to understanding when to @memo or not to @memo.

Maybe the advanced guides could also go into detail about how to performance profile this. Can we make this info easily accessible in the Ember Inspector maybe?

not change. Unfortunately, this is not really something that `@memo` can
circumvent, since there's no way to tell if a value has actually changed, or
to know which values are being accessed when the memoized value is accessed
until the getter is run.
Copy link
Contributor

Choose a reason for hiding this comment

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

In the future, we could change @tracked to check if the value is different potentially, and not invalidate unless it was. We could also add a new decorator which does this.

This would be very interesting to see!


## Detailed design

The `@memo` decorator will be exported from `@glimmer/tracking`, alongside
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that

  • this is a very @glimmer/tracking-specific feature
  • that is deeply connected to the tracking system
  • and embedding it there
    • standardizes the implementation across all apps
    • and also significantly reduces the time to market

But just so the question was asked: Was is considered to open up the tracking / tags API instead to allow user-land modules to implement @memo and other decorators?

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! I actually have another RFC in the works to do just this 😄 I wanted to land @memo because 1. it's a very small RFC, took half a day to write and I don't think it's super contentious and 2. I do think we should have a conventional solution for this in the framework, enough folks have asked for it that it would be a pain to have to install from the community.

But I 100% agree, we should have public tracking primitives that you could use to build something like @memo on top of!

@Gaurav0
Copy link
Contributor

Gaurav0 commented Jan 1, 2020

Please state explicitly:

  1. Will memoization work with nonserializeable properties (I suspect yes since it uses tags, rather than the actual property) ?
@tracked emberDataModel;

@memo get property() {
  return emberDataModel.property;
}

where emberDataModel is something that contains cycles and therefore cannot be JSON.stringifyed

and

  1. Will memoization allow recursion (I suspect not, this seems to be a cycle, although without infinite invalidation) ?
@tracked number;

@memo
get factorialOfNumber() {
  if (this.number <= 1) {
    return 1;
  }
  let currentNumber = this.number;
  this.number -= 1;
  return this.currentNumber * this.factorialOfNumber;
}

@pzuraq
Copy link
Contributor Author

pzuraq commented Jan 2, 2020

@Gaurav0

  1. I'm not sure I understand the question here. Why would the inability to serialize a value due to cycles affect memoization? Is this meaningfully different from 2?

  2. See the Cycles section of the RFC, which addresses this.

@Gaurav0
Copy link
Contributor

Gaurav0 commented Jan 2, 2020

  1. I'm not sure I understand the question here. Why would the inability to serialize a value due to cycles affect memoization? Is this meaningfully different from 2?

Most memoization functions you find online serialize their arguments to create a key for a cache.

@pzuraq
Copy link
Contributor Author

pzuraq commented Jan 2, 2020

Ah, yes this won’t be an issue with this decorator. It’s not memoizing based on arguments, it’s memoizing based on state consumed during calculation. This could potentially be expanded in the future, and we would address the issue then.

@tavosansal
Copy link

I think this is great but I feel the name is confusing. @memo does not tell me what this decorator is right away. Why not use @memoize? Seems like saving 3 characters does not help readability, specially since I see this decorator being used a lot.
@memo could also be confused with the actual word 'memo' (https://www.google.com/search?q=memo&oq=memo&aqs=chrome..69i57j0l4j69i61l3.1364j0j7&sourceid=chrome&ie=UTF-8) which can be confusing to people who don't natively speak English and might look up the word.

@danDanV1
Copy link

danDanV1 commented Jan 18, 2020

For developer ergonomics @memoize is much better. We never use sort hand like compu for computed. Personally, I was not familiar with the term 'memoization' before. I first thought it was a typo for memorization, but I searched the term and learned a lot. Pretty self explanatory and clear intent via @memoize

@lupestro
Copy link
Contributor

lupestro commented Mar 5, 2020

Is there an opportunity here for some instrumentation so the developer can determine if the memoization was "worth it"? Knowing the maximum (and perhaps minimum) number of times a memoized value is read between writes is useful data and is relatively cheap to calculate.

@ro0gr
Copy link

ro0gr commented Mar 6, 2020

Why not use @memoize?

In this case, we could consider "memoized" as well, cause it's aligned with "computed" and "tracked" stylistically. And memoize sounds more imperative imo.

I think memo is a great name, cause it leaves some space for everyone to decide whether it's memoized or memoize.

@pzuraq
Copy link
Contributor Author

pzuraq commented Mar 6, 2020

In this case, we could consider "memoized" as well, cause it's aligned with "computed" and "tracked" stylistically. And memoize sounds more imperative imo.

We discussed this on the core team a while back, sorry I haven't updated. This was a major consideration, keeping the theme consistent for all of the decorators was something we wanted to make sure we did. The consensus was that @memo and @memoized were a bit too technical as terms, and instead it would make sense to update it to @cached, since that describes more directly what is happening.

@lolmaus
Copy link

lolmaus commented Mar 13, 2020

Do we need:

  1. an equivalent of notifyPropertyChange? Not sure if it's useful with pull-based reactivity, but with classic Ember computed properties it was indispensable. I never liked it, but there were numerous use cases where it was necessary.

  2. Some API to force invalidation of memoized getter's cache?

@pzuraq
Copy link
Contributor Author

pzuraq commented Mar 13, 2020

@lolmaus I don't think so, and I think it could lead to a lot of antipatterns actually. I've already been seeing developers try to fallback to eventing and push-based reactivity at times when it wasn't really necessary, but they weren't familiar enough with the mental model of autotracking yet to see a better solution.

With computed properties, manual invalidation was sometimes necessary because the system could not express certain chains of dependencies. A great example of this (which I intend to publish a deep dive blog post about) is the createClassComputed setup from ember-macro-helpers: https://github.com/kellyselden/ember-macro-helpers#createclasscomputed

This is something that is trivial with autotracking - you don't even need to think about it, it already just works.

We can also always revisit this is the future if we find that there are certain patterns which require more manual/direct control. I think for those cases though, autotracking primitives (currently working on the RFC) will suffice, and don't need to integrate with @memo/@cached directly.

@lolmaus
Copy link

lolmaus commented Mar 14, 2020

@pzuraq Thank you for the explanations. 🙇‍♂️

@pzuraq pzuraq changed the title @memo @cached Mar 24, 2020
@lolmaus
Copy link

lolmaus commented Apr 9, 2020

I'm very concerned about this part of the RFC:

@memo may rerun even if the values themselves have not changed, since tracked properties will always invalidate even if their underlying value did not change. Unfortunately, this is not really something that @memo can circumvent, since there's no way to tell if a value has actually changed, or to know which values are being accessed when the memoized value is accessed until the getter is run.

Instead, we should be sure that the rules of property invalidation are clear, and in performance sensitive situations we recommend diff checking when assigning the property:

if (newValue !== this.trackedProp) {
  this.trackedProp = newValue;
}

This reminded me of my experience with AngularJS 1. I had a restricted page, a user should've been able to visit it only when they matched certain conditions. But the framework did not allow me to make this check in a central place (like Ember Route's beforeModel hook). Instead, I had to do the check on every page that had a link to the restricted page.

If I forgot to add a check to a link, users would be able to visit the restricted page when they shouldn't have been allowed to. This was also introducing multiple sources of truth: when refactoring the restrictions, it was easy to introduce inconsistencies.


Similarly, I would like to make a decision whether the getter should recompute or not when dependent value is the same — on the getter level and not on the level of each setter of each @tracked property it depends upon.

It's really not the responsibility of @tracked property setters to decide whether they should trigger recomputation on unchanged value or not.

  1. A @tracked property setter is simply not aware where that property is used. Forcing it to be aware means introducing tight coupling.

  2. A getter may depend on numerous @tracked properties, and each of those properties may have multiple setters. In order to implement a single decision "this getter should not recompute when its dependencies haven't really changed", I might need to implement lots of if (newValue !== this.trackedProp) checks, sprinkled all over the app. This means multiple sources of truth and a cause of inconsistencies in the future.

  3. A @tracked property may be used in multiple getters, and not all of them may be CPU-intensive.

  4. A getter may depend on @tracked properties indirectly, through other getters. It may be hard to track all dependant properties (and then all their setters!) in order to relieve computation burden.

  5. Setters may part of third-party addons/libraries. I don't want to be required to fork or monkey-patch them!

For these reasons, I believe it should be possible to decide whether a getter should recompute on unchanged dependencies — on the level of the getter and not its dependencies.

I can imagine something like this:

  @cached({shouldUpdateOnUnchangedDependencies: false})
  get foo() {
    return cpuIntensiveComputation(this.foo, this.thirdPartyService.bar.baz.quux);
  }

Please note that I do not insist that this functionality must be necessarily part of Ember core. I don't care if I import it from ember-source or from some addon.

What I do find important though is that the RFC should account for this use case and, if it's not supposed to be part of Ember core, provide a reference implementation for addons to pick up, rather than saying "you can use any third-party library, go figure it out yourself". That just doesn't work.


The reason why I started thinking about it is that I've got a few project where Redux store is used, but without connect, stateToComputed or shouldComponentUpdate.

I have a Redux store and various computed properties depend on values deeply nested in the store.

The problem is that any change to the store would cause all getters that depend on it to recompute — even though those getters depend on small, unchanged properties deeply nested within the store tree.

I'm looking for a way to prevent that.

With the old paradigm, I had to explicitly provide dependant keys to @compuuted. Though @computed also recomputes even when the values are the same, having the keys at hand should allow creating a custom decorator that would cache dependant values, compare them with new values and somehow stop propagation of the recomputation chain.

How do I do that with the new paradigm of @tracked and @cached?

@Gaurav0
Copy link
Contributor

Gaurav0 commented Apr 9, 2020

@lolmaus I agree. It should be the responsibility of the getter that does the heavy computation to diff its direct dependents. The relevant section should be changed. Instead, rewrite the getter like this:

  @cached
  get foo() {
     let quux = this.thirdPartyService.bar.baz.quux;
     if (this.bar === this.oldBar && quux === this.oldQuux) {
       return this.cachedFoo;
     }
     this.oldBar = this.bar;
     this.oldQuux = quux;
     this.cachedFoo = cpuIntensiveComputation(this.bar, quux);
     return this.cachedFoo;
  }

Of course, after going through all this trouble, you no longer need @cached. :)

@lolmaus
Copy link

lolmaus commented Apr 9, 2020

@Gaurav0, I've also been thinking about going this path, except that I don't like side-effects and the amount of boilerplate.

Instead, I was thinking about an API similar to @computed('foo.bar', 'baz.@each.{quux,zomg}'), reducing the amount of boilerplate. And a decorator could use a closure instead of mutating the class instance.

But as this RFC correctly admits, the ergonomics of providing keys are far from perfect. It would feel kinda a step back from the path Octane is taking.

I believe, that this RFC should attempt resolving it with tracking magic. There should be a way to simply opt into not recomputing if dependent values haven't changed. E. g. by passing true into the @cached decorator invocation.

Ideally, in addition to not performing the calculation, it should also interrupt the propagation chain.

And yes, as @Gaurav0 mentions, if this PR leaves this problem out of its scope and suggest using a third-party decorator for opting out of recomputation, such third-party decorator would defeat the purpose of this RFC. And it will do so with a small overhead, maybe at the cost of ergonomics.

@pzuraq
Copy link
Contributor Author

pzuraq commented Apr 9, 2020

@lolmaus what you describe is not possible in autotracking, unfortunately. The system is based around weak signals, not strong ones. By the time we get to a given @cached decorator, we've actually lost the information about what change caused the system to rerun, and there is no way to recover it.

Like @Gaurav0 pointed out, the best I think we can do here is some amount of manual cache validation. This could be built into a future version of the decorator, but we should experiment with the API first. I think getting the weak-cached version out sooner rather than later is probably good in the meantime.

@pzuraq
Copy link
Contributor Author

pzuraq commented Apr 9, 2020

The problem is that any change to the store would cause all getters that depend on it to recompute — even though those getters depend on small, unchanged properties deeply nested within the store tree.

This should not be a problem if you actually track root state in general. The root state is not the store - it's the small, unchanged properties. If you use a library like tracked-built-ins rather than signaling that the entire store has changed every time, then this will be much less of a concern in general.

This is part of the reason why I'm pushing for tracked built-ins to be part of the system. Root state should be always be annotated properly, and you shouldn't be signaling changes via parent objects. This is the flaw that Redux (and indeed React) have when it comes to state management - no way to narrow the scope of a change easily. We do have that, it just means you need to annotate the proper state.

@lolmaus
Copy link

lolmaus commented Apr 9, 2020

@pzuraq Thank you for acknowledging the legitimacy of my concerns, explaining why it can't be solved with autotracking magic and suggesting an alternative solution.

Using tracked-built-ins implies migrating away from ember-redux and shifting away from the paradigm of swapping immutable state objects to a paradigm of a singleton mutable state object, right?

This indeed seems reasonable and aligns well with Ember's traditional OOP approach. And this is exactly what @lifeart told me to do when I was whining about this RFC not helping me with my Redux woes.

Unfortunately, in my personal case, Redux is an integral part of all projects I'm currently involved with. The reasons Redux was chosen is because:

  • undo-redo functionality,
  • strict and purely declarative state structure, determined by file/folder structure with combineReducers,
  • strict regulations on how state content can be manipulated.

tracked-built-ins does not provide these (and many other Redux virtues) out of the box. I can't simply ember remove ember-redux; yarn add -D tracked-built-ins and live on. Migrating from Redux to tracked-built-ins implies building own state management from scratch, which is a helluva chore.

And I'm not the only one. ember-redux is in top 10% by downloads on EmberObserver. Its download count of 10K per month may be unimpressive, but, to put it into a perspective, that's 2.5× more than that of ember-orbit which is featured in every EmberConf.

Redux is too big of player to be turned a blind eye upon. The fact of Redux being troublesome/fiddly to use in Ember — will surely not help with increasing Ember's popularity on the frontend frontier.

PS I'm not saying you're wrong in any regard. And I'm certainly not saying this RFC is acting against Ember interest! But I do feel worried that the new autotracking paradigm would not cover all the cases of the former, soon-to-be-deprecated paradigm.

PPS My comment kinda implies that Redux is well-supported by the classic Ember paradigm. It isn't! ember-redux still requires writing a lot of boilerplate which feels alien to the Ember world, and having computed properties depend on state would cause each store change to trigger all CPs.

But boy, would it be awesome to simply inject a Redux service and have your getters directly depend on nested store properties, without funky business like stateToComputed, shouldComponentUpdate, connect, etc!

@pzuraq
Copy link
Contributor Author

pzuraq commented Apr 9, 2020

Using tracked-built-ins implies migrating away from ember-redux and shifting away from the paradigm of swapping immutable state objects to a paradigm of a singleton mutable state object, right?

This indeed seems reasonable and aligns well with Ember's traditional OOP approach. And this is exactly what @lifeart told me to do when I was whining about this RFC not helping me with my Redux woes.

I'm sorry if I made it seem this way, but that is definitely not what I meant!

What I moreso mean is that, ultimately, every piece of state in a JS app ultimately boils down to:

  • A property on an object
  • A value in a collection (Array, Set, Map, etc.)

So no matter what your implementation is, you should be able to effectively wrap it somehow, and once it's instrumented only track the deltas. This is the goal of autotracking in general. This is why tracked-built-ins is better than notifying for an entire object, every time one property changes.

For the case of Redux, we would want to do something similar. Basically, like you said here:

would it be awesome to simply inject a Redux service and have your getters directly depend on nested store properties, without funky business like stateToComputed, shouldComponentUpdate, connect, etc!

That should be completely possible! It would just mean wrapping Redux such that the edge/leaf properties entangle properly, and then notifying when they change. How we choose to notify and entangle depends on the library - for Redux, it may end up being more bespoke and require manual wrapping. Maybe it could use tracked-built-ins, maybe not, it all depends. Would have to dig in to figure it out, but it should be possible! 😄

This is what I mean when I keep saying that autotracking is paradigmless. It is a tiny wrapper on top of the state primitives in JS - properties and collections. Literally every type of data library is built on those, so every library should be wrappable in this way.

@lolmaus
Copy link

lolmaus commented Apr 10, 2020

So no matter what your implementation is, you should be able to effectively wrap it somehow, and once it's instrumented only track the deltas.

@pzuraq That's awesome! Can we come up with a specific solution?

I can imagine a decorator that follows ergonomics of @readOnly CP macro but stops propagation:

get expensive() {
  return cpuIntensive(this.store.foo.bar.baz.quux);
}

@myReadOnly('store.foo.bar.baz.quux')
quux;

get cheap() {
  return cpuIntensive(this.quux);
}

Alternatively, a decorator could be applied to a getter, with arguments similar to @computed:

@myComputed('store.foo.bar.baz.quux')
get cheap() {
  return cpuIntensive(this.store.foo.bar.baz.quux);
}

The latter feels kinda like alternative to this RFC, with the Classic API. :trollface:

@pzuraq, how can @myReadOnly and @myCopmputed stop the propagation of the recomputation chain? Being an Ember user, I can only think of dirty solutions, e. g. conditionally reassigning the value to another property on the class instance.

Not sure how tracked-built-ins can help with the Redux, since I can't patch Redux to use tracked-built-ins internally. Or do you mean creating a singleton mutable copy of the Redux state — and mutating it every time this.store is replaced with a new immutable copy? That would be trivial to do with an observer, but how do I do it without? And how expensive is doing it for all the state tree and not just for the individual bits like in the example above.

PS OK, now that you have clearly explained the boundaries of what autotracking can and cannot do, this matter indeed feels as an offtopic in this thread.

But I feel really uncomfortable hopping onto the autotracking train, leaving Classic paradigm behind for good, without matters like this being fully resolved.

@pzuraq
Copy link
Contributor Author

pzuraq commented Apr 10, 2020

I’d need to spend some time digging into how Redux is implemented, but I would naturally want to put the autotracking on the store-side, rather than the consumer side. Ideally you would just inject the store and reference it like a normal property:

@cached
get cheap() {
  return cpuIntensive(this.store.foo.bar.baz.quux);
}

How we do that really again depends on how the store is implemented. It gets a bit trickier if it really is immutable, since we’ll have to do diffing ourselves then or somehow keep track of what exactly has changed. I would likely do it via a Proxy though, for starters, since that keeps us pretty flexible.

@rwjblue
Copy link
Member

rwjblue commented May 18, 2020

Now that #615 is landed, I think this is ready for a revamp...

@rwjblue rwjblue added the T-framework RFCs that impact the ember.js library label May 18, 2020
buschtoens added a commit to buschtoens/ember-rfc176-data that referenced this pull request Aug 10, 2020
This adds a mapping from `{ memo } from '@glimmer/tracking'` to `Ember._memo`.

This enables the [`ember-memo-decorator-polyfill`][polyfill] for [RFC 566 "@memo decorator"][rfc-566] to work without any [`patch-package`][patch-package] hackery.

[polyfill]: https://github.com/ember-polyfills/ember-memo-decorator-polyfill
[rfc-566]: emberjs/rfcs#566
[patch-package]: https://github.com/ds300/patch-package/issues
@buschtoens
Copy link
Contributor

You can already start experimenting with this using the ember-cached-decorator-polyfill. 👩‍🔬

- RFC PR: https://github.com/emberjs/rfcs/pull/566
- Tracking: (leave this empty)

# @cached
Copy link
Contributor

@MelSumner MelSumner Jan 29, 2021

Choose a reason for hiding this comment

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

Suggested change
# @cached
# Add a `@cached` decorator

I think the title should be more descriptive.

@rwjblue
Copy link
Member

rwjblue commented Feb 12, 2021

We discussed this in todays core team meeting, and are generally in favor of moving forward here. There are a couple of things that we should update with before FCP'ing:

  • Add some API documentation to the "How do we teach this"
  • Add some prose (likely to both API docs and design) to explain that we think you should not just "sprinkle @cached" around. Most operations should not need to be cached, and you should likely only use @cached when you know the operation is expensive (e.g. complicated computations, reading DOM in a way that forces layout, etc).

@pzuraq - Would you mind updating with ^ info?

@rwjblue
Copy link
Member

rwjblue commented Feb 19, 2021

Thanks for updating @pzuraq!

We discussed this at todays core team meeting, and are still very much in favor of moving forward. Moving this into final comment period...

@rwjblue rwjblue merged commit 993ee1e into master Feb 26, 2021
@rwjblue rwjblue deleted the memo-decorator branch February 26, 2021 19:15
```

In this example, the `fullName` getter will be memoized whenever it is called,
and will only be recalculated the next time the `firstName` or `lastName`
Copy link

Choose a reason for hiding this comment

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

@pzuraq is the opposite true-- if your cached getter relies on zero tracked properties, will the getter just calculate once and then not recalculate?

Copy link

Choose a reason for hiding this comment

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

@roneesh, a @cached getter that does not read any @tracked properties (directly or through other getters) — will never be recalculated.

But I believe this behavior is not opposite to the one you quoted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Final Comment Period T-framework RFCs that impact the ember.js library
Projects
None yet
Development

Successfully merging this pull request may close these issues.