diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2023-10-01 17:30:39 -0300 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2023-10-01 17:30:39 -0300 |
commit | d2174a01dda63ba233cbcdf48bb70ed50127d54d (patch) | |
tree | dd8fc157001f4728ed0d8bfc879b49ec563bf172 /src/data/things | |
parent | ab7591e45e7e31b4e2c0e2f81e224672145993fa (diff) |
data: obliterate composite.js explainer
Poor (read: largely outdated) code documentation is worse than no code documentation. The various infrastructural systems specially designed for hsmusic should get more dedicated reference material, but that can't well be written before the systems are tested and used for longer. The compositional data processing style has just about settled, but it's still very young (compared to, say, the overarching data- to-page flow, content functions, or the HTML and content template systems).
Diffstat (limited to 'src/data/things')
-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 => |