"Recompute with...", or: a way to "climb the ladder of abstraction"

Like @bumbeishvili I’m against introducing any more non-standard syntax. It’s irritating enough that the import keyword in Observable mimics ES6 imports but only works for notebooks, raising the barrier of entry to Observable.

Dependencies between cells aren’t always as clear-cut as one would like them to be. E.g., some cells might modify properties of an object stored in a different cell without triggering updates. My recommendation would be to create and treat notebook instances as blackboxes with an API surface. Observable’s Runtime should already provide the necessary tools, so what’s left imo is some syntactic sugar / helpers:

  • outline potential use cases and limitations,
  • provide a framework for different approaches (functional, generator, …),
  • prevent misuse and misconceptions by exposing a “safe” API (i.e. don’t allow modification of a notebook instance in a way that may cause side effects).
3 Likes

Those are good points. The motivations for not introducing new syntax seem quite reasonable. From this point of view, a solution where a regular cell is converted to a “cell with abstracted inputs” seems attractive.

What I’m imagining now is the possibility to have something like the following expression (in this example, I’m taking inspiration from my own actual work on network error correction):

performTest = Notebook.abstract(['numRepairedPackets', 'chart'], ['lossProbability', 'groupSize']]

performTest would now be a callable, to which replacement inputs can be supplied:

d3.range(0, 100).map((probPerc) => {
  const perf = performTest({
    lossProbability: probPerc*0.01,
    groupSize: 25
  });
  return {probPerc, perf};
})
// Output:
// [ {probPerc: 0, perf: { numRepairedPackets: ..., chart: ... }]
// , {probPerc: 1, perf: { numRepairedPackets: ..., chart: ... }]
// , {probPerc: 2, perf: { numRepairedPackets: ..., chart: ... }]
// , ...

Another important point of agreement is that side effects such as mutations should remain contained into the “abstracted” evaluation, and never be observable by other cells of the notebook. In my view, triggering updates in the notebook would be very much a bug, not a feature. Rather, it should become a sort of “automatic refactoring”, where a part of the notebook is cut out into an independent instance, with an API dictated by the selected inputs and outputs. The runtime’s knowledge of the dependency graph would be what makes this feasible.

I have to say though, if it becomes necessary to separate the work into two notebooks, one importing the other, it looks to me like there’s a big chance that this feature becomes so inflexible and unergonomic that it may very seldom be worth using. Could someone make an example of applying a collection of replacement inputs, like the above, but with an import-based mechanism?

This makes sense to me. It would be nice to expand the syntax to support importing arbitrary modules using a URL since that’s how it works in browsers. If Observable adopts the with syntax above, they could change the import-with syntax to use this instead:

import { foo, bar } from '@user/notebook' with { baz, quux = 42 }

This would make the actual import statement fully standards compliant and makes the statement more in line with with syntax elsewhere in the notebook.