diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/data/things/composite.js | 333 |
1 files changed, 0 insertions, 333 deletions
diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 7e068dce..51525bc1 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -15,339 +15,6 @@ import { withAggregate, } from '#sugar'; -// Composes multiple compositional "steps" and a "base" to form a property -// descriptor out of modular building blocks. This is an extension to the -// more general-purpose CacheableObject property descriptor syntax, and -// aims to make modular data processing - which lends to declarativity - -// much easier, without fundamentally altering much of the typical syntax -// or terminology, nor building on it to an excessive degree. -// -// Think of a composition as being a chain of steps which lead into a final -// base property, which is usually responsible for returning the value that -// will actually get exposed when the property being described is accessed. -// -// == The compositional base: == -// -// The final item in a compositional list is its base, and it identifies -// the essential qualities of the property descriptor. The compositional -// steps preceding it may exit early, in which case the expose function -// defined on the base won't be called; or they will provide dependencies -// that the base may use to compute the final value that gets exposed for -// this property. -// -// The base indicates the capabilities of the composition as a whole. -// It should be {expose: true}, since that's the only area that preceding -// compositional steps (currently) can actually influence. If it's also -// {update: true}, then the composition as a whole accepts an update value -// just like normal update-flag property descriptors - meaning it can be -// set with `thing.someProperty = value` and that value will be paseed -// into each (implementing) step's transform() function, as well as the -// base. Bases usually aren't {compose: true}, but can be - check out the -// section on "nesting compositions" for details about that. -// -// Every composition always has exactly one compositional base, and it's -// always the last item in the composition list. All items preceding it -// are compositional steps, described below. -// -// == Compositional steps: == -// -// Compositional steps are, in essence, typical property descriptors with -// the extra flag {compose: true}. They operate on existing dependencies, -// and are typically dynamically constructed by "utility" functions (but -// can also be manually declared within the step list of a composition). -// Compositional steps serve two purposes: -// -// 1. exit early, if some condition is matched, returning and exposing -// some value directly from that step instead of continuing further -// down the step list; -// -// 2. and/or provide new, dynamically created "private" dependencies which -// can be accessed by further steps down the list, or at the base at -// the bottom, modularly supplying information that will contribute to -// the final value exposed for this property. -// -// Usually it's just one of those two, but it's fine for a step to perform -// both jobs if the situation benefits. -// -// Compositional steps are the real "modular" or "compositional" part of -// this data processing style - they're designed to be combined together -// in dynamic, versatile ways, as each property demands it. You usually -// define a compositional step to be returned by some ordinary static -// property-descriptor-returning function (customarily namespaced under -// the relevant Thing class's static `composite` field) - that lets you -// reuse it in multiple compositions later on. -// -// Compositional steps are implemented with "continuation passing style", -// meaning the connection to the next link on the chain is passed right to -// each step's compute (or transform) function, and the implementation gets -// to decide whether to continue on that chain or exit early by returning -// some other value. -// -// Every step along the chain, apart from the base at the bottom, has to -// have the {compose: true} step. That means its compute() or transform() -// function will be passed an extra argument at the end, `continuation`. -// To provide new dependencies to items further down the chain, just pass -// them directly to this continuation() function, customarily with a hash -// ('#') prefixing each name - for example: -// -// compute({..some dependencies..}, continuation) { -// return continuation({ -// '#excitingProperty': (..a value made from dependencies..), -// }); -// } -// -// Performing an early exit is as simple as returning some other value, -// instead of the continuation. You may also use `continuation.exit(value)` -// to perform the exact same kind of early exit - it's just a different -// syntax that might fit in better in certain longer compositions. -// -// It may be fine to simply provide new dependencies under a hard-coded -// name, such as '#excitingProperty' above, but if you're writing a utility -// that dynamically returns the compositional step and you suspect you -// might want to use this step multiple times in a single composition, -// it's customary to accept a name for the result. -// -// Here's a detailed example showing off early exit, dynamically operating -// on a provided dependency name, and then providing a result in another -// also-provided dependency name: -// -// withResolvedContribs = ({ -// from: contribsByRefDependency, -// into: outputDependency, -// }) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: [contribsByRefDependency, 'artistData'], -// compute({ -// [contribsByRefDependency]: contribsByRef, -// artistData, -// }, continuation) { -// if (!artistData) return null; /* early exit! */ -// return continuation({ -// [outputDependency]: /* this is the important part */ -// (..resolve contributions one way or another..), -// }); -// }, -// }, -// }); -// -// And how you might work that into a composition: -// -// Track.coverArtists = -// compositeFrom([ -// doSomethingWhichMightEarlyExit(), -// -// withResolvedContribs({ -// from: 'coverArtistContribsByRef', -// into: '#coverArtistContribs', -// }), -// -// { -// flags: {expose: true}, -// expose: { -// dependencies: ['#coverArtistContribs'], -// compute: ({'#coverArtistContribs': coverArtistContribs}) => -// coverArtistContribs.map(({who}) => who), -// }, -// }, -// ]); -// -// One last note! A super common code pattern when creating more complex -// compositions is to have several steps which *only* expose and compose. -// As a syntax shortcut, you can skip the outer section. It's basically -// like writing out just the {expose: {...}} part. Remember that this -// indicates that the step you're defining is compositional, so you have -// to specify the flags manually for the base, even if this property isn't -// going to get an {update: true} flag. -// -// == Cache-safe dependency names: == -// -// [Disclosure: The caching engine hasn't actually been implemented yet. -// As such, this section is subject to change, and simply provides sound -// forward-facing advice and interfaces.] -// -// It's a good idea to write individual compositional steps in such a way -// that they're "cache-safe" - meaning the same input (dependency) values -// will always result in the same output (continuation or early exit). -// -// In order to facilitate this, compositional step descriptors may specify -// unique `mapDependencies`, `mapContinuation`, and `options` values. -// -// Consider the `withResolvedContribs` example adjusted to make use of -// two of these options below: -// -// withResolvedContribs = ({ -// from: contribsByRefDependency, -// into: outputDependency, -// }) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: ['artistData'], -// mapDependencies: {contribsByRef: contribsByRefDependency}, -// mapContinuation: {outputDependency}, -// compute({ -// contribsByRef, /* no longer in square brackets */ -// artistData, -// }, continuation) { -// if (!artistData) return null; -// return continuation({ -// outputDependency: /* no longer in square brackets */ -// (..resolve contributions one way or another..), -// }); -// }, -// }, -// }); -// -// With a little destructuring and restructuring JavaScript sugar, the -// above can be simplified some more: -// -// withResolvedContribs = ({from, to}) => ({ -// flags: {expose: true, compose: true}, -// expose: { -// dependencies: ['artistData'], -// mapDependencies: {from}, -// mapContinuation: {into}, -// compute({artistData, from: contribsByRef}, continuation) { -// if (!artistData) return null; -// return continuation({ -// into: (..resolve contributions one way or another..), -// }); -// }, -// }, -// }); -// -// These two properties let you separate the name-mapping behavior (for -// dependencies and the continuation) from the main body of the compute -// function. That means the compute function will *always* get inputs in -// the same form (dependencies 'artistData' and 'from' above), and will -// *always* provide its output in the same form (early return or 'to'). -// -// Thanks to that, this `compute` function is cache-safe! Its outputs can -// be cached corresponding to each set of mapped inputs. So it won't matter -// whether the `from` dependency is named `coverArtistContribsByRef` or -// `contributorContribsByRef` or something else - the compute function -// doesn't care, and only expects that value to be provided via its `from` -// argument. Likewise, it doesn't matter if the output should be sent to -// '#coverArtistContribs` or `#contributorContribs` or some other name; -// the mapping is handled automatically outside, and compute will always -// output its value to the continuation's `to`. -// -// Note that `mapDependencies` and `mapContinuation` should be objects of -// the same "shape" each run - that is, the values will change depending on -// outside context, but the keys are always the same. You shouldn't use -// `mapDependencies` to dynamically select more or fewer dependencies. -// If you need to dynamically select a range of dependencies, just specify -// them in the `dependencies` array like usual. The caching engine will -// understand that differently named `dependencies` indicate separate -// input-output caches should be used. -// -// The 'options' property makes it possible to specify external arguments -// that fundamentally change the behavior of the `compute` function, while -// still remaining cache-safe. It indicates that the caching engine should -// use a completely different input-to-output cache for each permutation -// of the 'options' values. This way, those functions are still cacheable -// at all; they'll just be cached separately for each set of option values. -// Values on the 'options' property will always be provided in compute's -// dependencies under '#options' (to avoid name conflicts with other -// dependencies). -// -// == To compute or to transform: == -// -// A compositional step can work directly on a property's stored update -// value, transforming it in place and either early exiting with it or -// passing it on (via continuation) to the next item(s) in the -// compositional step list. (If needed, these can provide dependencies -// the same way as compute functions too - just pass that object after -// the updated (or same) transform value in your call to continuation().) -// -// But in order to make them more versatile, compositional steps have an -// extra trick up their sleeve. If a compositional step implements compute -// and *not* transform, it can still be used in a composition targeting a -// property which updates! These retain their full dependency-providing and -// early exit functionality - they just won't be provided the update value. -// If a compute-implementing step returns its continuation, then whichever -// later step (or the base) next implements transform() will receive the -// update value that had so far been running - as well as any dependencies -// the compute() step returned, of course! -// -// Please note that a compositional step which transforms *should not* -// specify, in its flags, {update: true}. Just provide the transform() -// function in its expose descriptor; it will be automatically detected -// and used when appropriate. -// -// It's actually possible for a step to specify both transform and compute, -// in which case the transform() implementation will only be selected if -// the composition's base is {update: true}. It's not exactly known why you -// would want to specify unique-but-related transform and compute behavior, -// but the basic possibility was too cool to skip out on. -// -// == Nesting compositions: == -// -// Compositional steps are so convenient that you just might want to bundle -// them together, and form a whole new step-shaped unit of its own! -// -// In order to allow for this while helping to ensure internal dependencies -// remain neatly isolated from the composition which nests your bundle, -// the compositeFrom() function will accept and adapt to a base that -// specifies the {compose: true} flag, just like the steps preceding it. -// -// The continuation function that gets provided to the base will be mildly -// special - after all, nothing follows the base within the composition's -// own list! Instead of appending dependencies alongside any previously -// provided ones to be available to the next step, the base's continuation -// function should be used to define "exports" of the composition as a -// whole. It's similar to the usual behavior of the continuation, just -// expanded to the scope of the composition instead of following steps. -// -// For example, suppose your composition (which you expect to include in -// other compositions) brings about several private, hash-prefixed -// dependencies to contribute to its own results. Those dependencies won't -// end up "bleeding" into the dependency list of whichever composition is -// nesting this one - they will totally disappear once all the steps in -// the nested composition have finished up. -// -// To "export" the results of processing all those dependencies (provided -// that's something you want to do and this composition isn't used purely -// for a conditional early-exit), you'll want to define them in the -// continuation passed to the base. (Customarily, those should start with -// a hash just like the exports from any other compositional step; they're -// still dynamically provided dependencies!) -// -// Another way to "export" dependencies is by using calling *any* step's -// `continuation.raise()` function. This is sort of like early exiting, -// but instead of quitting out the whole entire property, it will just -// break out of the current, nested composition's list of steps, acting -// as though the composition had finished naturally. The dependencies -// passed to `raise` will be the ones which get exported. -// -// Since `raise` is another way to export dependencies, if you're using -// dynamic export names, you should specify `mapContinuation` on the step -// calling `continuation.raise` as well. -// -// An important note on `mapDependencies` here: A nested composition gets -// free access to all the ordinary properties defined on the thing it's -// working on, but if you want it to depend on *private* dependencies - -// ones prefixed with '#' - which were provided by some other compositional -// step preceding wherever this one gets nested, then you *have* to use -// `mapDependencies` to gain access. Check out the section on "cache-safe -// dependency names" for information on this syntax! -// -// Also - on rare occasion - you might want to make a reusable composition -// that itself causes the composition *it's* nested in to raise. If that's -// the case, give `composition.raiseAbove()` a go! This effectively means -// kicking out of *two* layers of nested composition - the one including -// the step with the `raiseAbove` call, and the composition which that one -// is nested within. You don't need to use `raiseAbove` if the reusable -// utility function just returns a single compositional step, but if you -// want to make use of other compositional steps, it gives you access to -// the same conditional-raise capabilities. -// -// Have some syntax sugar! Since nested compositions are defined by having -// the base be {compose: true}, the composition will infer as much if you -// don't specifying the base's flags at all. Simply use the same shorthand -// syntax as for other compositional steps, and it'll work out cleanly! -// - const globalCompositeCache = {}; const _valueIntoToken = shape => |