🏠 back to Observable

Check if a cell is defined

I’m hoping to check if an Observable cell has been defined, but running into an issue using the eval() function as I would in a JavaScript setting. Any thoughts on this one? Would be helpful for writing activities for new learners. Thanks!

Cells act a little differently than normal JavaScript variables, they don’t really have “global scope” that you would see in other JS environments. You have to specifically reference the target cell’s name in the eval()'s cell definition in order to have that variable in scope.

This doesn’t work:

eval("typeof value_1") // "undefined"

because value_1 isn’t in the scope of that cell. To fix this, if you reference the value_1 cell in that cell, then it acts as expected:

value_1, eval("typeof value_1") // "number"

Though that syntax may be a little confusing, so here’s a the same thing but a little more verbose:

{
  value_1;
  return eval("typeof value_1")
}

But in general, I wouldn’t recommend using eval() to check to see if a cell is defined. cell_name === undefined will work in all cases, unless cell_name or any of its ancestors throws an error. In that case, you’ll get a RuntimeError: cell_name could not be resolved. There’s unfortunately no easy way to avoid this case 100% of the time, unless you manually handle any errors that could happen in cell_name and all of its ancestors.

I hope that makes sense!

2 Likes

I doubt that you’ll have much luck. Observable checks the required dependencies statically and constructs a wrapper around your cell code. If you don’t explicitely access a cell value, it won’t be available.

I’d recommend to add a debugger statement to any cell that contains references, and inspect the transpiled code in your browser’s dev tools.


If you want to go fully meta and experimental, have a look at

This notebook is based on a hack that @bgchen mentioned to me (:wave:) and of course absolutely not recommended.

2 Likes

Totally makes sense! Unfortunately, I’m trying to pass a string that contains the cell name as an argument to a function. More detail on that here: https://observablehq.com/d/be15a2fa71d0e49d

If you have any thoughts on that or workarounds, they’re much appreciated!

Oh wow, there’s some dense code in there, but it does get access to the variables in a notebook!

Oof, and thanks so much!

variables = Array.from(runtime._variables)
  .filter(v => v._name !== null)
  .map(v => [modules.get(v._module), v._type === 2 ? `(${v._name})` : v._name])
  .reduce((o, [m, v]) => ((o[m] || (o[m] = [])).push(v), o), {})

Do you happen to know if there’s a way to trigger the runtime to re-evaluate when new cells are added? The code works great, but if you define new cells, those aren’t captured in runtime._variables.

A kindof funny workaround…

variables = Array.from(runtime._variables)
  .concat(now)
  .filter(v => v._name !== null && v._type === 1)
  .map(v => v._name)

Another very-not-recommended and very-hacky approach: scouting the DOM for all the .observablehq–cellname elements:

is_defined = {
  const m = [];
  const t = setInterval(update, 200);
  invalidation.then(() => clearInterval(t));

  update();
  return a => m.includes(`${a} = `);

  function update() {
    m.length = 0;
    for (const e of document.getElementsByClassName("observablehq--cellname"))
      m.push(e.textContent);
  }
}

it’s almost guaranteed to be unreliable :slight_smile: And it works only for cells that would display their names (like cell_1 = “hello”).

2 Likes

You may want to utilize a MutationObserver for that, instead of polling.

Hey, at least it has linebreaks! :smiley:

I can think of two options. You could either proxy (i.e., wrap) the setters on the relevant Sets / Arrays (assuming the references are kept inside the Runtime; you’ll have to check that yourself). Or you could listen for editor events (try adding and changing a cell in the notebook):

Since you already have access to the Runtime, you might also be able to add an Inspector and check for the name there.

1 Like

Ah, super cool! This is slick, but would only capture changes (so if a person re-loads their notebook, defined cells wouldn’t be checked). I think I’ll try out the above approaches and see where I can get!

So, I think I’ve managed to (mostly) combine these approaches, though I need to run (any) cell to get them to all evaluate. I may be misinterpreting something about how mutables get defined – do you have any sense as to why I need to evaluate the mutable variables cell to get it to return anything (on page load, it’s blank). Thanks so much for your help!

You need an additional import to trigger the scenario that allows capturing the Runtime on load. Reimporting from the current notebook suffices:

import {runtime as dummy} from '12e6887297f406bc'

I suggest that you go over the documentation in my notebook to understand how/why the capture works, keep an eye on the JS console, and also play around with the DEBUG option (while your dev tools are open).

Also, I might not have been clear enough that I really don’t recommend this approach. If you describe your use case / overarching goal in more detail, then perhaps we can find a better solution that doesn’t involve awful hacks like this one. :slight_smile:

1 Like

Thanks so much (again!). If you’re open to providing feedback // ideas, the use case for this is constructing teaching notebooks that check student work. When implemented, it would look something like this:

// Check if the cell named `my_variable` stores a value that matches an answer
checkValue("my_variable")

Here is a notebook that I wrote showing how it works in JavaScript using eval(), but not with Observable cells. If you have the time, I’d love to hear your thoughts!

I’d go for a mix of MutationObserver and dynamic notebook imports. Will try to assemble an example tomorrow.

1 Like

Here’s another approach, using side-effects and a mutable which stores the last non-error value of the cell you want to check, and null otherwise:

This requires two additional cells for each cell you want to observe dynamically, but it should be less brittle than some of the other approaches described above.

4 Likes

Thanks! The alternative of just defining the cells is where I started – it really just requires cell_name = undefined for each prompt, which isn’t terrible (the rest of this approach is mostly ripped off of @anjana). https://observablehq.com/d/3e5dbd7eab223d0a

I was mostly just curious if there was an available workaround, and @mootari and @Fil have provided some great ideas!

Just to clarify, I was suggesting something like this in your appendix:

mutable maybeProtests = []
{
  mutable maybeProtests = protests;
  invalidation.then(() => mutable maybeProtests = []);
}

Then instead of

check_answer({protests}) 

You say

check_answer({protests: maybeProtests})

This technique could be extended to track the other answers, too.

2 Likes

And here’s the technique applied to your notebook:

1 Like

Awesome, thanks for taking the time to put that example together! I feel like I’m circling the same problem, but is there a way to avoid having to repeat the same step for each prompt (e.g., the redundant update("var_name", varname, invalidation)? For example, if I could iterate through Object.keys(answers) and perform the update for each one? Feels like I’ll run into the same issue (which, again, isn’t a major issue – mostly just curious at this point!).

Thanks again!

There’s not currently a way to avoid a separate update(…) cell for each variable; combining the cells would mean that a single error would cause multiple updates to fail. It’s the best I could think of for now. :slight_smile: If we add error handling to Observable JavaScript then there will be better ways to express this.

2 Likes