Overload cached for non-class use - #19
Conversation
|
|
99f6bb9 to
abd1b27
Compare
|
|
||
| Our documentation / guides currently don't have much of anything on reactivity, and over the years, it's been useful to talk about reactive primitives as things _outside_ of classes, and compose/wrap them in to refactoring boundaries (classes, components, etc). | ||
|
|
||
| [RFC#1071](https://github.com/emberjs/rfcs/blob/master/text/1071-overload-tracked-for-non-class-use.md) gave us `tracked()` for _root state_ outside of classes, but there is no ergonomic equivalent for _derived state_ -- today, memoized derivation outside of a class requires either a class with a `@cached` getter, or dropping down to the memoization primitives from [RFC#615](https://github.com/emberjs/rfcs/blob/master/text/0615-autotracking-memoization.md). |
There was a problem hiding this comment.
derived state is not the same as cached state. do not mix these up
There was a problem hiding this comment.
Reworded in 4a80599: derived state is plain functions and needs no API — cached() is opt-in memoization of a derivation (same relationship @cached has to ordinary getters). Fixed in Summary, Motivation, usage headings, and How We Teach.
Implements the standalone form proposed in NullVoxPopuli/rfcs#19 -- the memoization companion to RFC 1071's overloaded tracked (emberjs#21471). cached(fn, options?) returns a read-only CachedValue backed by createCache/getValue. options.equals (default Object.is) retains the previous value's identity when a re-computation produces an equivalent result; options.description labels the cache for debugging. The decorator form is unchanged: a legacy decorator invocation always receives three arguments, so the two forms cannot collide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
||
| ## Motivation | ||
|
|
||
| Our documentation / guides currently don't have much of anything on reactivity, and over the years, it's been useful to talk about reactive primitives as things _outside_ of classes, and compose/wrap them in to refactoring boundaries (classes, components, etc). |
There was a problem hiding this comment.
Updated in 1fe2145 — Motivation now links ember-learn/guides-source#2219 instead of claiming the guides are sparse.
|
|
||
| Our documentation / guides currently don't have much of anything on reactivity, and over the years, it's been useful to talk about reactive primitives as things _outside_ of classes, and compose/wrap them in to refactoring boundaries (classes, components, etc). | ||
|
|
||
| To be clear about terms: _derived state_ is not the same as _cached state_, and derived state outside of a class needs no new API -- any plain function or getter that reads reactive values is already derived state, and staying a plain function should remain the default. `cached` is specifically about _caching_ such a derivation: only re-running the function when tracked state it previously read has changed. As with the `@cached` decorator, this is an opt-in optimization for expensive computations, not the way to derive. |
There was a problem hiding this comment.
we don't need to clarify this, because it's not an ambiguation. the docs in ember-learn/guides-source#2219 explain this (I think).
There was a problem hiding this comment.
Removed the paragraph in 1fe2145 (also trimmed the echo of it in How We Teach).
|
|
||
| Enabling `cached` to be used outside of a class makes it a good tool for demos[^demos] for memoizing expensive computations in function-based APIs, such as _helpers_, _modifiers_, or _resources_ (or even in module space)[^apps]. They also provide a benefit in testing as well, since tests tend to want to assert that expensive computations do not re-run unnecessarily. | ||
|
|
||
| This is not too dissimilar to the [Autotracking Memoization primitives in RFC#615](https://github.com/emberjs/rfcs/blob/master/text/0615-autotracking-memoization.md) (`createCache` / `getValue`). Making `cached` work outside of classes provides the same benefit without requiring 2 imports from a `primitives` path to use. This RFC intends to provide a tool enabling us to de-emphasize (and potentially later deprecate) the `@glimmer/tracking/primitives/cache` import path for app developers[^primitives-future]. |
There was a problem hiding this comment.
we don't need to deprecate this. we should probably scrap this paragraph and mention it in the Appendix
There was a problem hiding this comment.
Scrapped from Motivation in 1fe2145; now an Appendix section ("Relationship to RFC#615's memoization primitives") with no de-emphasize/deprecate language — primitives stay as-is.
| } | ||
| ~~~ | ||
|
|
||
| Unlike RFC#1071's `TrackedValue`, there is no `set`, `update`, or `freeze` -- a `CachedValue` is derived entirely from the tracked state its function reads, so it is _born_ a `ReadOnlyReactive`. |
There was a problem hiding this comment.
Reworded in 4f8eb4e — "has no storage of its own; its value comes entirely from the tracked state its function reads, so it is a ReadOnlyReactive from the start."
| this.#cache = createCache(() => { | ||
| let next = fn(); | ||
|
|
||
| if (this.#hasPrevious && options.equals(this.#previous, next)) { |
There was a problem hiding this comment.
using equals here makes no sense. So this RFC should not propose equals as part of the construction API.
We could add it later, but it's unclear if there would ever be a benefit.
There was a problem hiding this comment.
(it makes no sense, because calling fn at all is the expensive part, so if we're going to call it to then not use the value, that's silly)
There was a problem hiding this comment.
Removed equals from the proposal in 4f8eb4e — options are now just { description }, with a short deferred note in the Appendix. Also dropped from the implementation PR (emberjs/ember.js@ea6ee0e).
| } | ||
| ``` | ||
|
|
||
| (the real implementation would live lower in the reactivity system, because less abstraction layers are speedier) |
There was a problem hiding this comment.
Dropped in 4f8eb4e.
|
|
||
| (the real implementation would live lower in the reactivity system, because less abstraction layers are speedier) | ||
|
|
||
| The function passed to `cached` is only re-invoked when tracked state it previously read has changed -- exactly the memoization semantics of the `@cached` decorator from [RFC#566](https://github.com/emberjs/rfcs/blob/master/text/0566-memo-decorator.md). |
There was a problem hiding this comment.
in this whole RFC, let's not use the word memoization
There was a problem hiding this comment.
Done in 4f8eb4e — "caching" throughout; the only remaining instance is inside RFC 615's URL slug.
|
|
||
| Enabling `cached` to be used outside of a class makes it a good tool for demos[^demos] for caching expensive computations in function-based APIs, such as _helpers_, _modifiers_, or _resources_ (or even in module space)[^apps]. They also provide a benefit in testing as well, since tests tend to want to assert that expensive computations do not re-run unnecessarily. | ||
|
|
||
| `cached`-as-non-decorator was prototyped in [Starbeam](https://starbeamjs.com/guides/fundamentals/functions.html) (as `CachedFormula`) and similar utilities have been available for folks to try out in ember via [ember-resources](https://github.com/NullVoxPopuli/ember-resources) and [reactiveweb](https://github.com/universal-ember/reactiveweb). |
There was a problem hiding this comment.
re: 'reactiveweb', which one? if not real, remove
re: resources -- not related -- that is a very different concept
There was a problem hiding this comment.
Removed both in e2a8f89 — prior art now cites Starbeam's CachedFormula only.
| ); | ||
| } | ||
|
|
||
| interface CachedValue<Value> extends ReadOnlyReactive<Value> { |
There was a problem hiding this comment.
let's not add a new interface. get: () => Value should be part of read-only reactive, and its an oversight that it's not
There was a problem hiding this comment.
Done in e2a8f89 — no new interface; get added to ReadOnlyReactive (noted as an RFC 1071 oversight) and cached() returns ReadOnlyReactive<Value>. Implementation updated to match (emberjs/ember.js@49ccf38): get is on the interface, the class is internal-only.
|
|
||
| Caching a computation over local state in a template. | ||
|
|
||
| ```gjs |
There was a problem hiding this comment.
this example is bad and contrived. let's not do this
There was a problem hiding this comment.
Removed in e2a8f89; kept only the module-state example.
| Caching a computation over module state. | ||
| This is already common in demos. | ||
|
|
||
| ```gjs |
Derived-state companion to RFC 1071 (overloaded tracked). Re-uses the Reactive/ReadOnlyReactive interfaces defined there; cached(fn, options) returns a read-only CachedValue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derived state is just plain functions and needs no API; cached() is opt-in memoization of a derivation. Reword Summary, Motivation, usage headings, and How We Teach accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Motivation references the in-flight reactivity guides (ember-learn/guides-source#2219) instead of claiming docs are sparse - remove the derived-vs-cached clarification paragraph (the guides cover it; not an ambiguation) - move the RFC 615 relationship to the Appendix, without any de-emphasize/deprecate intent Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- remove the equals option from the proposed API entirely (calling fn is the expensive part; re-running it to discard the result is silly); noted as deferred in the Appendix - say caching, not memoization, throughout - reword the 'born a ReadOnlyReactive' sentence - drop the implementation-would-be-lower aside Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- no new CachedValue interface; add get to ReadOnlyReactive instead (an RFC 1071 oversight) and return ReadOnlyReactive from cached() - apply suggested guides sentence (drop 'over the years') - Starbeam only in prior art; resources are a different concept - remove the contrived nested-let template example Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
e2a8f89 to
1d1bcb0
Compare
e41fbbd
into
NullVoxPopuli:nvp/cached-overloaded
Summary
Adds a new RFC proposing an overload for
cached(from@glimmer/tracking) so it can be used outside of classes — the derived-state companion to RFC 1071 (overloadedtracked).emberjsremote and re-worded forcachedthroughout.Reactive/ReadOnlyReactiveinterfaces defined in RFC 1071;cached(fn, options)returns aCachedValue extends ReadOnlyReactive(noset/update/freeze— it is born read-only).options.equals(defaultObject.is) preserves referential identity when a re-computation produces an equivalent value;options.descriptionfor debugging.cached(fn)as the app-facing replacement forcreateCache/getValuefrom@glimmer/tracking/primitives/cache(RFC 615).The file is named
text/0000-…for now; it should be renumbered to the upstream PR number when proposed on emberjs/rfcs (RFC 1071 followed the same convention).Implementation PR: emberjs/ember.js#21537
🤖 Generated with Claude Code