promises and mutable in

this notebook raises two questions.

  1. why do not the promises in the runTest operator resolve without the await prefix?
    the invocations cross cell boundaries, but without the explicit resolution the three variables end up bound to promises.

  2. what is the proper pattern to incrementally compute the results object and inform the table cell of modifications?
    even incremental notification would suffice. as currently written, the runTest operator augments theresultsobject’s content for each invocation, but it is never notified and therefore the table never reflects the new content. if, on the other hand results is made mutable, assigning it to itself effects an infinite loop.

Hi @lomoramic. Do these two notebook answer your questions?

as to (1), the first reference is what established the expectation, that a promise would be resolved across a cell boundary.

Observable implicitly awaits promises across cell boundaries, so you often don’t need to deal with a promise directly.

that a later paragraph suggests an await might be necessary for intra-cell references, suggest there are circumstances which do not resolve,. as was, in fact, required.

You can use the await operator to pause the execution of code in the current cell while you wait for a promise to resolve

the question remains, why? what distinguishes the use in question from a “promise across a cell boundary”?

as to (2), those idioms suffice for a case where the constitutents are known before hand.
the goal in this case is to be able to introduce additional cells into the notebook where each need know just what to augment with its results, rather than implementing a “generator-like” control structure which is aware of all constituents and polls them for results.

The implicit await of promises only applies to cell values, not local variables. So, if cell a is defined as a promise, and cell b references a, then cell b only runs after a resolves.

a = new Promise((resolve) => setTimeout(() => resolve("hello"), 1000)
b = a + " world"

In your case, runTest is a function (an async function), not a Promise. If you wish to reference the result of runTest, you must either await it in the same cell that you call it:

  const result = await runTest(…);

Or you can save the result as a top-level cell, and then reference it from another cell, taking advantage of implicit await:

result = runTest(…)

That’s perhaps true of generator cells, but not generator functions (see explainer). If your runTest function were an async generator, it could yield intermediate values to display test progress, and you can call it as many times as you like with different arguments. Here’s a toy example:

async function* timer(seconds) {
  for (let i = 0; i < seconds; ++i) {
    yield i;
    await Promises.delay(1000);
timer(10) // count to ten
timer(5) // count to five

yes, true, but runTest is async exactly because it has to await the three calls to GET. without the awaits the respective variable ends up bound to a promise - even though the GET calls span cells.
is that because the GET implementation delegates to the Cells.Cell.getResource and that is not recognized as something which could return a promise?

It’s fine that runTest is async — that just means it returns a promise. What I’m saying is that Observable’s implicit promise resolution only happens when you reference a cell value (that’s a promise) from another cell. So if you call a function that returns a promise within your cell, that doesn’t count: even if that function is defined in another cell, the cell’s value is the function, not the promise it returns.

Anyhow, maybe you could elaborate on what you want to happen here, and then I could sketch out how that would look?

in abstract terms, invert your declarations.
rather than have the table declare the cells from which it collects data, have those cells push data to it.

in this case, that would mean adding variants of the “test: all” cell and having each cell’s result reflected in the table without declaring this to the table, but just by augmenting the table input wth the respective test cell result. that is the purpose of the current results cell.

  • generators (function or otherwise) do not look like they accomplish this.
  • async/await is not central to this, just incidental.

One of Observable’s central ideas is the value of each cell depends only on the other cell it references, similar to pure functions. So what you describe where one cell (the one declaring the test) mutates another cell (the one showing a table summarizing all tests) is not the natural choice in Observable; the natural choice would be that the table references all of the test cells to summarize their current state. To extend the toy timer example:

async function* timer(seconds) {
  for (let i = 0; i < seconds; ++i) {
    yield i;
    await Promises.delay(1000);
timer1 = timer(10)
timer2 = timer(5)
timers = [timer1, timer2] // collect timers
  <tbody>${ => html`<tr><td>${value}</td></tr>`)}</tbody>

As you point out, this means that you need to have both the cell that declares the test and another cell that collects the tests to produce the summary. But that way each cell’s value depends only on the other cells it references.

It is possible to do exactly what you describe and push data the other way, but it is more complicated: you use mutation to modify the value of another cell and the invalidation promise to clean up when cells are re-run (or edited or deleted). I’m happy to show you how to do that, but it’s not what I’d recommend. Would you like to see anyway?

the limitation being that the cell which collects needs to know the identity of the others statically. i am looking for a way to abstract this relation, not to have to express it by name at compile-time, but rather to delay the binding until run-time.
were it possible either for cells to introspect, to tell another cell “here i am, you depend on me”, or for a cell to compute its references by examining other cells, that would work. the former is the effect which the results cell tries to create. it just does not work out.
is there is some way to use an invalidation promise to effect this?

Thanks for the additional context. The quick fix would be to use a mutable, as you’re already doing. To trigger a reactive assignment to a mutable, you must use an assignment expression. For example:

mutable foo = 0
  mutable foo = mutable foo + 1;

Mutating a mutable value (such as an object) does not trigger a reactive evaluation; it’s the identifier (foo) that is mutable, not the value. In this way Observable’s mutable is similar to JavaScript’s const, which does not prevent you from mutating a value, only from reassigning the identifier to a new value.

If your mutable is an object with properties for each test, the recommended way to assign it a new value is to copy on write:

  mutable results = {...mutable results, [viewName]: status};

Alternatively, you can mutate the value and then reassign the mutable to itself:

  mutable results[viewName] = status;
  mutable results = mutable results;

This latter approach is not recommended because mutating the value in-place may cause nondeterministic behavior in other cells.

(Also, you typically want to avoid referencing foo in the same cell that you assign to mutable foo, as this can easily cause an infinite loop. Hence why I only reference mutable results in the assignments above.)

the “copy on write” behaves as desired, thank you.
the alternative:

  mutable results[viewName] = status;
  mutable results = mutable results;

was the first this i tried. (note the commented section in runTest.)
at first without the second mutable.
judging by the indication that the table cell was constantly regenerating, that assignment led to an infinite loop - whether with or without the second mutable.
this was the original motivation for this question.

1 Like