A bit confused about 'mutable', etc

Can someone help me figure out the right way to make a little interactive plot which takes the moved positions of Bezier curve control points, and propagates those changes back to a cell where they are first defined? (I want to do similar interfaces which are more complicated … just trying to get something simple working first.) I have this notebook https://beta.observablehq.com/@jrus/bezier-segment and I’ve been fiddling around with it, but I feel like I don’t really yet understand how ‘mutable’, ‘viewof’, etc. are supposed to work, and the documentation notebooks about it aren’t really giving me a conceptual understanding. I can copy/paste their examples and those work, but I don’t really get what is going on. (Maybe those can also be extended with a bit more technical description at the bottom? Or maybe I’m just missing something / not reading the right bit in the right way.)

4 Likes

The quick fix

You’re code is almost spot-on, and the bez value does indeed update when you drag the handles. The only problem you had in your code, is that when your display cell (the one track turns bez into a string format) references by mutable bez instead of just bez, and hence it’s not updated when bez is updated. That is, the only edit required is changing the following:

  • change '[' + (mutable bez).map(p => '\n [' + p + ']') + '\n]'
  • to '[' + (bez).map(p => '\n [' + p + ']') + '\n]'

See the minor edit in action here: https://beta.observablehq.com/@kelleyvanevert/bezier-segment.

To understand: there are 2 ways of referencing mutable cells

Now, to understand what’s going on…

Thinking mutables away for a second, what Observable does under the hood, is track dependencies between cells. If some cell a references another cell b, which Observable knows by just statically analyzing it’s code (as far as I know), then Observable notes this dependency, and every time b is updated, it re-evaluates a as well.

Sometimes you want to organize your notebook in such a way that you can just update your cell-defined variables, as if they were ‘normal javascript variables’, i.e. what you’re doing with bez. But, by referencing such a variable (necessary in your plot code for the assignment), as stated above, Observable notes a dependency of plot on bez, and hence, plot would be re-evaluated when bez changes. This is not what you want.

Hence, Observable introduces the mutable keyword. Now, you can define a cell variable to be mutable, i.e. with your mutable bez = [ ... ], and there are 2 (!) ways to referencing this cell:

  1. A ‘normal’ reference, just bez. This works as usual, it tracks the implied dependency on bez's value, and whenever bez's value changes, the referencing cell is re-evaluated as well. This is the one you need for your displaying cell.
  2. A ‘mutable’ reference: mutable bez. Here, the catch is that Observable does not track the dependency. Hence, you can use this kind of reference to write the code of your plot cell, which you don’t want re-evaluated whenever bez's value changes. You can use this kind of reference both for getting the value of bez, as well as for an assignment (as you do correctly in your code).

How mutable and viewof relate

The explanation above, and in particular the two distinct ways of referencing a mutable cell, are enough to use mutables correctly. If you want to understand how this exactly ties together with the viewof operator, though, we have to go a bit ‘deeper’ into implementation details.

When you define a cell with the code, say, viewof a = html`<input />`, what Observable ‘actually does’ is create two separate cells, which you can think of as follows:

  1. node_view_of_a = html`<input />`
  2. a = Generators.input(node_view_of_a) – where Generators.input is an Observable std lib function that basically just always gives back the current value of the node. (See https://beta.observablehq.com/@mbostock/five-minute-introduction)

Then, there are 2 (!) ways of referencing a:

  1. By just a, as usual
  2. By viewof a, which for the translated two cells above means node_view_of_a. Hence, it ‘actuall refers to a different cell’, namely the one that defines the view of a instead of just a itself.

If this sounds familiar now, it is, because mutables are in a way just syntactic sugar for using viewof. (See my initial attempt to create something like mutable using just viewof: https://beta.observablehq.com/@kelleyvanevert/settable-variables/2 .)

When you define a cell with mutable a = ...b..., what Observable ‘actually does’ is create two separate cells, which you can thinkg of as follows:

  1. mutable_interface_for_a = html`<input />`, but then maybe just slightly more elaborate, and with initial value, etc.
  2. a = Generators.input(mutable_interface_for_a)

The two ways of referencing a (as explained above, a and mutable a) then translate to:

  1. Just a
  2. mutable a translates to, in the case of usage, just mutable_interface_for_a.value, and in the case of assignment mutable a = c, something like
mutable_interface_for_a.value = c;
mutable_interface_for_a.dispatchEvent(new CustomEvent('input'))

Obviously, this can be done in a single cell using viewof: the ‘mutable interface for a’ is just the viewof a.

I hope this explains! :slight_smile:

7 Likes

Thanks for the explanation, @kelleyvanevert! To partly reiterate, I want to show how both viewof and mutable can be implemented without special syntax—both operators are syntactic sugar.

viewof

Example:

viewof foo = html`<input type="range">`

Equivalent implementation without the viewof operator:

viewof_foo = html`<input type="range">`
foo = Generators.input(viewof_foo)

Any reference to viewof foo in other cells is equivalent to viewof_foo. To implement view references without special syntax, instead of:

viewof foo.type

Say:

viewof_foo.type

mutable

Example:

mutable foo = 0

Equivalent implementation without the mutable operator:

initial_foo = 0
mutable_foo = new Mutable(initial_foo)
foo = mutable_foo.generator

(You could simplify slightly by inlining the initial value of foo in the mutable_foo definition, but this is the pedantic equivalent implementation that also works if the initial value is a promise or a generator.)

Any reference to mutable foo in other cells is equivalent to mutable_foo.value. To mutate the mutable value without special syntax, instead of:

{ mutable foo = 42; }

Say:

{ mutable_foo.value = 42; }

Setting the mutable’s value causes the mutable’s generator’s current promise to resolve. See the implementation here:

Likewise, to non-reactively reference the value of foo, instead of:

{ return mutable foo; }

Say:

{ return mutable_foo.value; }
10 Likes

To address the higher-level question as to what practice would be best here…

Views are intended to simultaneously define a graphical interface for the reader (possibly interactive) and a programmatic interface for the rest of the notebook.

In your notebook on Bézier segments, you might use a view to define the control points of the Bézier segment, such that these control points are visible as an array to the other cells of the notebook, while the reader sees and can interact with a visual representation of the Bézier segment. I use a similar technique here to represent the subject polygon:

Views are easiest when there is a primary interface to the interactive value. If you want multiple synchronized views, where each display controls a shared underlying value, then you need to designate a primary cell to define the value. That cell can be visual if you want, but it probably makes more sense to have it as a data cell, and then have all the views modify the data. Here’s a notebook on that:

Mutables are intended to represent shared state that can be mutated by any cell in your notebook. Mutables work best when each cell either gets (reads) or sets (writes) the mutable value, or neither, but not both. You can have multiple cells setting the mutable by assigning to mutable foo, and multiple cells getting the mutable with reactive references to foo, but you don’t want to reference foo in the same cell that you assign to mutable foo because it might loop infinitely.

If you use a mutable to represent the control points in your notebook on Bézier segments, the challenge is that the interactive displays need to both read and write the values of the control points. (If one display modifies the control points, you want to update all the displays.) You could probably achieve this if you used the this value to prevent the views from being reinitialized when the control points change, but I’m not sure that would be simpler than using views plus event listeners, and it would certainly be more complicated than having a primary view.

5 Likes

Thanks, both of you. I’ll keep fiddling with it for a while.

So, I do want the plot to re-render when bez changes. I’m not sure if that necessarily means the whole cell should be reevaluated, or just the redraw function should be triggered, but eventually I would like to be able to have several plot cells in a row (showing different things) but with the controls linked so that changing any one of them updates the data and then triggers them all to redraw.

Short note: You can update your plot in two ways: (1) by simply re-evaluating the cell (and then you don’t need the code that does the d3 update), (2) by using code that does a d3 update, as you currently do. The funky thing in this situation, is that your plot displaying cell also functions as an input cell, and hence you can’t choose option (1) because then the moment you start dragging, the cell would re-evaluate.

There are several things to say on the matter of tying cells together into an interactive notebook:

  • As Mike pointed out, it’s good to conceptualize with the notions of view and mutable. However, instead of a view, I would rather call things made with viewof an input, because that is their primary difference with normal cells: they can receive user input. The similarity (sameness?) between viewof and mutable is they they both can receive input: the former, via user input, the latter, programmatically. But, as your notebook shows, you can also program a cell that receives user input without viewof, but just by saving to a mutable. Hence, I would propose to first distinguish cells on their function in a notebook:
    • (user) input cells receive and store input (either by viewof or by saving to a mutable);
    • programmatically mutable cells are made with mutable and act as ‘imperative variables that hold mutable data’;
    • display cells are normal cells that just happen to display/visualize stuff.
  • There are a number of ways of linking several of these three kinds of cells up into an interactive notebook:
    • Mike’s clipping notebook essentially has 1 (viewof) user input cell at the top, and then a whole slew of display cells that work on the input cell’s data. This works, because there is only 1 input cell. Diagrammically, we have the situation:
      input cell -> display cells / rest of notebook
      
    • Sometimes you want to have the input cell also display more stuff, that is computed outside of it, as the case here, or, you want to link up multiple input cells; you could then use the following pattern:
      input cells -> mutable cell -> feedback
                                ( \-> display cells / rest of notebook)
      
      Here, each input cell (whether viewof or not) sets/changes the (single, data-storing) mutable accordingly, and then the mutable changes are propagated back in one of two ways:
      1. Either the input cells actually depend on the mutable (i.e. with bez), this is best for small inputs, so that re-evaluation is not so intensive, and not so complex.
      2. Or via an additional cell, that exists solely to backpropagate the changes by inspecting and changing the viewof inputcell. I have found this to be typically useful if the input cell is some complicated d3 vizualization, because d3 is designed to be updated this way:
        updating_cell = {
          const data = bez; // your mutable
          d3.select(inputcell_svg) // for `viewof inputcell_svg = ...`
            .selectAll(...).data(...) // etc..
          return `this cell exists solely to backpropagate the
             mutable data to the various input cells' vizualisations`;
        }
        
        This method is particularly useful if you don’t want to re-evaluate your input cells, which I can imagine it the case for your bezier curve input cell.
    • Another way to link up input cells that has been proposed and used a number of times on Observable, but which I find to be a bit awkward and messy, is to have them listen to each other’s input events, and then sync.
    • And probably more …
  • Then, there is the problem of horizontally aligning these things, especially if they’re complicated user input cells; because notebooks are of course inherently vertical. It seems to me there are two distinct ways of dealing with this:
    1. Forget it, and just write your code in such a way that you can make it as a single vizualisation / input / whatever.
    2. Write your code as usual, and then in the cells that you want to have horizontally aligned, don’t return the dom node as usual, but keep it from attaching to the document by returning say a singleton array [domnode]. Then, write the horizontally aligning cell as:
      html`<div style="display: flex;">
        ${view1[0]}
        ${view2[0]}
        ${view3[0]} <!-- etc -->
      </div>`
      
      I made a quick example of this technique for your bezier curve, here: https://beta.observablehq.com/@kelleyvanevert/horizontally-aligned-user-inputs.
1 Like

Quick side note:

You might want to add a bit more text description to https://beta.observablehq.com/@mbostock/standard-library or GitHub - observablehq/stdlib: The Observable standard library. (in that one, Generators.observe and Generators.input are each described as “…”). As it is now the the code itself as the main documentation, and it takes decent understanding of JavaScript promises to follow.

[Also, for the purpose of more targeted links (e.g. would be nice here to directly link to the generators section of the standard library notebook), can you folks consider explicitly naming all of the cells with section titles in the first-party documentation notebooks?]

1 Like

@jrus Oops! That had been on my to-do list for a while, but thanks to your encouragement I’ve finally updated the API reference. Hope this helps.

2 Likes

Here’s a new notebook that demonstrates how to implement synchronized interactive views:

It expands on the Views are Mutable Values notebook, but with a custom SVG display rather than using HTML input elements.

3 Likes

Thanks Mike, this one looks like a winner. I’ll try it whenever my 21-month-old gets to sleep and I can spend some more time fiddling. :slight_smile:

Then I’ll also go back and more carefully read the other replies in this thread.

1 Like

Hmm, somehow I got my notebook (with various unpublished changes) into a state where it hangs the interface before I can edit it to change/fix anything.

Any advice about dealing with that? Maybe there could be a way to load a notebook in such a way that all of the cell editing works but nothing gets evaluated/rendered?

Safe mode: What to do when accidentally added a non-terminating while loop?

1 Like

Yep, just found that one (before going to eat). Thanks!

@mbostock: it doesn’t seem to like this:

// Listen for input events on the view until the cell is disposed.
(viewof bez).addEventListener("input", update);
return Generators.disposable(svg.node(), () => {
  (viewof bez).removeEventListener("input", update);
});

where bez is an instance of View from @mbostock/synchronized-views

I currently have those lines commented out at https://beta.observablehq.com/@jrus/bezier-segment


Okay, I figured out my problem, which was that I was calling update still inside the dragged callback and then running viewof bez.value = viewof bez.value; inside of update(), but then that was recursively triggering update again with the event listener. Newly published version should nominally work.

2 Likes

In Martien’s Timer I do use both gets and sets of the boolean running in the same cell. I’ve run both in the re-evaluate and the infinite loop pitfall.

The mutable running = !mutable running saved my day.

Always open to more elegant ways, though.

1 Like

I literally signed up for this forum just for an explanation like this and those below by @mbostock. Thank you so much, both of you!

2 Likes