Idiomatic Notebook 2.0 coding

With the soft release of the web interface to Notebooks 2.0, I thought it would be good to hear people’s views on idiomatic approaches to code design in notebooks, as I think the move to vanilla JS exposes interesting issues around code scope and entry points.

In a traditional app, there would typically be single point of entry to start the app, making it clear how the top-level of code design composes lower level functions with appropriate scoping.

In a notebook however, there can be multiple ‘entry points’ via many cells. In Notebooks 1.0, anonymous code blocks were a natural way of providing scoped top-level entry points. For example,

{
  const fmt = new Intl.DateTimeFormat(undefined, { dateStyle: "long" });
  return fmt.format(new Date());
}

With 2.0, we have to use a different approach. Expressions seem like a natural fit if the entry point is a single line. This works well, for example with a single Plot expression. But if the entry point requires multiple statements, we are forced to take a different approach. For example we could create a code block with a top-level named function and then call it. This also requires us to pass the returned value through display if we want to see it in the cell, and has the disadvantage of adding to the top-level namespace for a potentially one-off function call.

function today() {
  const fmt = new Intl.DateTimeFormat(undefined, {dateStyle: "long"});
  return fmt.format(new Date());
}
display(today());

There are also arguments for distinguishing the function behaviour from its invocation in separate cells. This would be natural if called more than once in a document. It has the additional advantage of allowing the function call to be an expression, so avoiding the need for display() (but vulnerable to accidentally turning it into a statement with a semicolon):

function today() {
  const fmt = new Intl.DateTimeFormat(undefined, {dateStyle: "long"});
  return fmt.format(new Date());
}
today()

Or we could make the function anonymous and call it via an IIFE ( IIFE - Glossary | MDN ). Because this is now a one-line expression we don’t need display():

(() => {
  const fmt = new Intl.DateTimeFormat(undefined, {dateStyle: "long"});
  return fmt.format(new Date());
})()

While it is more compact and directly models the old 1.0 behaviour, it doesn’t feel very beginner-friendly. From experience, the students I teach can easily get lost in the nested and unnested brackets and often don’t have a clear mental model of what’s going on.

What are people’s views on the most idiomatic approaches to multiple entry points in a 2.0 notebook?

I can think of another option for structuring our code: treat each notebook like module, and heavily refactor into different notebook, using imports to handle scope.

This is kind of hypocritical coming from me, since I’m very, very guilty of making oversized notebooks that are big balls of dependency spaghetti. In my defense switching between notebook tabs adds juuust enough friction to make it hard to stay in that sweet flow of prototyping in Observable.

The display function also returns the value that was passed to it, so there are also other possibilities to structure the code. I experimented a little with that here:

This includes approaches where most of us would probably know they’re bad ideas without trying them, but I figured that it can still be enlightening to actually see them side-by-side with other approaches.

The direct equivalent in Vanilla JavaScript would be:

{
  const fmt = new Intl.DateTimeFormat(undefined, {dateStyle: "long"});
  display(fmt.format(new Date()));
}

Did you want something different? In other words, you can use block statements to create scopes that hide local variables from other cells, if that’s what you want; you don’t need functions.

In general, though, I tend to not worry about hiding variables from other cells, since top-level variables don’t have to have unique names unless you want to reference them from other cells.

It‘s always good to think about the interface of the code if you intend for it to be reused and understood. But creating many notebooks has a higher maintenance burden, especially since imports aren’t live and you therefore don’t see the effects of edits instantly across tabs. (I’d love for imports to be live but it is tricky to implement; I’m not sure when or if we’ll be able to ship live imports.) In my experience notebooks feel suited for more lightweight and iterative development as opposed to more formally maintained (and tested and documented) libraries published to npm.

The forthcoming dataflow panel will also provide more visibility into the live values of top-level variables; the hope is you won’t need to call display() just for debugging purposes, and instead can reserve use of display() for stuff you really want the reader to see inline/interspersed with other content.