Reaching toward reusable computed property subchains


#1

Every good question starts with a story.

In our app, we do electronics calculations. We have a broad variety of data, constraints and targets, that the user can supply. We generate circuits that will satisfy their constraints and targets. We then output the data various graphs, the schematic, and other kinds of info, like noise, that the user might want to know about the circuit.

We used to do this with a whole raft of functions, which we re-ran large subsets of when something changed. We now use computed values, which form an acyclical digraph several layers deep. It’s deep and broad enough that we’re not sure exactly how many. I explain it to management as a huge headless spreadsheet. I explain to electrical engineers that it’s a little like working in LabVIEW in the browser, but not really. :slight_smile:

This has turned out to be a very good thing. Not every input affects every output, and the user is looking at one kind of output display at a time.

  • When the input changes, the dirty bits flip in a flood down the chain - but not all of the properties go dirty - only those dependent upon what changed.
  • When a chart draws, it asks for only one of the outputs, which asks for other intermediate data, flooding up the chain. Not all the intermediate or final data is recalculated - only “dirty” data and only data that’s actually needed to draw that chart.
  • When the user flips to the next chart, only a few stray items need to be recalculated.

This makes the whole thing pretty efficient and the circuit itself effectively does the global analysis of what subset of the calculations are actually needed to get an answer. This is really important when the chain of calculations is too big to visualize all together.

We will one day find a way to do a runtime analysis on these chains. (I could use advice on instrumenting Ember for this.) There may be whole hunks of the graph that always calculate together. These would benefit from coalescing into a single chain of pure functions, avoiding some of the inherent inefficiencies of observer systems. Starting from computed properties, we can use our (pretty comprehensive) calculation test suite as the exerciser for that measurement, and in the meantime what we have works.

That’s the story so far. Now for the current chapter.

Sometimes, we need to do calculations for circuits that are just a little different. We may snap capacitor and resistor values from abstract calculated values to commercially available values, at a granularity of 24/decade or 96/decade . (“I can’t give you a 1.189k resistor - would you settle for 1.2k?”) We may push various values to the limits of their tolerances for worst-case scenarios so users can see an envelope of the range of likely real-world results.

To do this, we essentially need to perform a region of the digraph of computed values against a different set of inputs. In my application, I want to isolate a connected chunk of all that computation, drive copies of it with different circuits, and then compare and combine the results. The only rough place in doing this is hooking an instance of this “computation component” to that instance’s dependencies.

Every computed value is based on some member of the enclosing class, possibly supplied as a creation parameter and a string representing a path from that object . If anything within that path changes, the computed value “goes dirty”. Suppose you’ve passed something via

create ( {model: this.myObject} )

and your computed property watches "model.foo.bar". If the outside world starts using a different object for myObject or something changes what baz inside of bar references, the computed property will not notice. However, if the reference stored in your model member is replaced or the foo or bar references change, the “dirty” bit flips.

Based on this, I can see three feasible avenues to pursue, the most palatable first:

  • Common Partial Structures (a.k.a. interfaces :grinning: ):

    • Computed values are relative to a property on the class instance. If different instances are fed a reference to a different model object with similar sub-structure, the computed value dependency strings could remain the same, even though they are pointing different places in each instance. For a controlled boundary, we could set up aliases for what we need to reference and then reference everything inside the “component” to those aliases.

    • It is possible that, in the original code, the computed property parameter binding stretched well up the tree to respond to more far-reaching changes. If we are to use an interface, we would need to make aliases in the object we are receiving of the higher-level structures. Then we can bind properties against the aliases, which will go “dirty” when the things they stand in for go “dirty”.

    • Using “edge connector” and “socket” alias structures is very tidy, but it isn’t by any means free. Two extra aliases have to cost you something, but maybe it’s affordable.

  • In the absence of common structure:

    • If computed dependencies are set up during the this._super(...arguments) portion of instance initialization or on receiveAttrs during object creation, we might be able to use a creation parameter containing our binding parameter strings to setup or re-setup the dependency list in the instance.
  • If component dependencies are wired in during class creation…

    • Anything we do at the class level to change the behavior of a single class is, by definition, a non-starter. We don’t need to do one thing, change the definition, and then do something else, all in sequence, but set up a static digraph in advance containing several instances of the subchain in different places to run against different inputs.

    • There’s still a way we can do it at the class level. We can use an IIFE to generate different classes containing the same code but using different binding parameter values, passed in a closure to the class definition when it executes EmberObject.extends().

Okay, so I just discovered in a conversation with @pzuraq today that I could use computed values in a plain TS class. Who knew? :man_shrugging: I suspect this doesn’t affect which of the above are viable, though.

What can anybody tell me about the viability of any of these approaches? In particular, is any of them a non-starter?