Published
Edited
Aug 31, 2021
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
define("D", 41) // Defines "D" as 1.
.and( // .and joins sets of definitions. Later definitions win conflicts.
define("E", a => a + 1, "D")) // Defines "E" as the result of a function applied to "D".
.query("E") // .query gets the value for a name from a set of definitions.
Insert cell
Insert cell
mutable sys1 = // create the system
// "mutable" lets this run much like you'd expect non-Observable JS code to run
// IMPORTANT: Run this cell to (re)initialize the system (play button to the right).
define("B", s => 'I' + s, "F")
.and(define("F", 'nitial value!'))
.and(define("A", (a, b) => a + b, "B", "C"))
.and(define("C", 1))
.and(define("myfn", a => 1, "F"))
.and(define("undef"))
Insert cell
{ // Attach an observer to the system that will update our div when 'B' changes.
const div = html`<div/>`
mutable sys1 = mutable sys1.observe('B', v => div.innerHTML = `Contents of B: ${v}`)
return div
}
Insert cell
// Run this cell (play button to the right) to modify B.
mutable sys1.and(define("B", "updated!"))
Insert cell
// A possibly-helpful view of the system above. Query values are to the far right.
compact(sys1)
Insert cell
viz(sys1)
Insert cell
md`### Using Views`
Insert cell
Insert cell
define("a", 1)
.and(define("double_ex", a => 2 * a, "a")) //.query('double_ex') yields 2
.and(define("double", VIEW, "double_ex", "a")) // "double" is a view of "double_ex" where "a" is a variable
.and(define("out", "double", "a"))
.query('out')
Insert cell
Insert cell
sys2 = define("a", 1)
.and(define("double_ex", a => 2 * a, "a"))
.and(define("double", VIEW, "double_ex", "a")) // Example from above
.and(define("A", [1, 2, 3]))
.and(define("B", a => [...a, 4, 5], "A"))
.and(define("map_ex", (A, f) => A.map(f), "B", "double")) // map_ex depends directly on B and double, but
.and(define("map", VIEW, "map_ex", "A", "double")) // we want to see how it relates to A,
// a dependency of B.
.and(define("C", [0, 1, 2]))
.and(define("triple_ex", a => 3 * a, "a"))
.and(define("triple", VIEW, "triple_ex", "a"))
.and(define("out", "map", "C", "triple"))
.and(define("D", (a, b) => [...a, ...b], 'A', "B"))
Insert cell
Insert cell
Insert cell
viz(sys2)
Insert cell
sys2.query("double_ex")
Insert cell
sys2.query("double")
Insert cell
sys2.query("map_ex")
Insert cell
sys2.query("map")([0, 2], a => 4 * a)
Insert cell
sys2.query("map")
Insert cell
sys2.query('out')
Insert cell
Insert cell
sys5.query('A')
Insert cell
sys5 = define("B", (s) => {throw Error('runtime'); return "E" + s}, "F")
.and(define("A", (a, b) => a + b, "B", "C"))
.and(define("C", 1))
.and(define("F", "F"))
.and(define("myfn", a => 1, "A"))
.and(define("undef", "undefined_input"))
Insert cell
sys5.and(define("test", VIEW, "A")).query("test")
Insert cell
Insert cell
md`Defining recursive functions is possible, though verbose. Here, fib(n) is built out of fib(fib, n), and separate examples for the base case and general case are created.`
Insert cell
fib = define("a", 1)
.and(define("fn_ex", a => 2 * a, "a"))
.and(define("fn", VIEW, "fn_ex", "a"))
.and(define("base_case_n", 1))
.and(define("first_general_case_n", 2))
.and(define("base_case_ex", (f, n) => 1, "fn", "base_case_n"))
.and(define("base_case", VIEW, "base_case_ex", "fn", "base_case_n"))
.and(define("fib_ex", (f, n) => n < 2 ? 1 : f(f, n-1) + f(f, n-2), "base_case", "first_general_case_n"))
.and(define("fib_explicit_rec", VIEW, "fib_ex", "base_case", "first_general_case_n"))
.and(define("example_n", 6))
.and(define("fib_explicit_rec_out", (fib, n) => fib(fib, n), "fib_explicit_rec", "example_n"))
.and(define("fib", VIEW, "fib_explicit_rec_out", "example_n"))
.and(define("fib_out", "fib", "example_n"))
Insert cell
viz(fib)
Insert cell
Insert cell
Insert cell
Insert cell
define = (name, fn, ...args) => {
if (typeof fn === 'function' && fn.length != args.length)
throw Error(`${fn} takes ${fn.length} args, ${args.length} given`)
return DefinitionStack([{name, fn, args}]) // (This is a definition object inside the list.)
}
Insert cell
function DefinitionStack (definitions, observers=[]) {
const Hole = idx => ({idx, __is_a_hole: true})
const ContainsHole = fn => ({fn, __contains_a_hole: true})

const query = (name, holes=[]) => {
if (holes.includes(name)) return Hole(holes.indexOf(name))
const def = definitions.find(e => e.name == name)
if (!def) throw new NotDefined(name)
let {fn, args} = def
let old_fn = fn
fn =
fn === VIEW ? VIEW
: args.length > 0 || typeof fn === 'function' ? fn
: () => old_fn // wrap values as functions. Zero-argument functions are not permitted as values.
args = args.map(arg => holes.includes(arg) ? Hole(holes.indexOf(arg)) : arg)
let undefined_inputs = []
const resolve = (name, holes) => {
try {
return query(name, holes)
} catch (e) {
log(e)
if (e instanceof NotDefined) {
undefined_inputs.push(name)
} else {
throw e
}
}
}
if (fn === VIEW) {
let [view_fn, ...view_holes] = args // could refactor to use proper view definitions
return resolve(view_fn, [...view_holes, ...holes]).fn
}
log(5, name, fn, args)
fn = args.length == 0 || typeof fn === 'function' || fn.__is_a_hole ? fn : resolve(fn, holes)
args = args.map(arg => arg.__is_a_hole ? arg : resolve(arg, holes) )
log(1, name, fn, args, undefined_inputs)
if (undefined_inputs.length > 0)
throw new InputsNotDefined(undefined_inputs.join(','))
let err = [fn, ...args].find(v => v.__is_an_error)
if (err !== undefined) return NodeError([...err.chain, name], err.error)

if (args.some(arg => arg.__is_a_hole || arg.__contains_a_hole) || fn.__is_a_hole) {
console.log(3, name, fn, args)
return ContainsHole((...view_args) => {
console.log(4, name, fn, args, view_args)
return (fn.__is_a_hole ? view_args[fn.idx] : fn)
(...args.map(arg => arg.__is_a_hole ? view_args[arg.idx]
: arg.__contains_a_hole ? arg.fn(...view_args)
: arg))
})
}

try {
return (fn.__contains_a_hole ? fn.fn : fn)(...args)
} catch (e) {
return NodeError([name], e)
}
}

// Naively update all observers whenever a new definition is created.
observers.map(observer => observer.callback(query(observer.name)))
return {
definitions,
observers,
and: stack2 => DefinitionStack([ ...stack2.definitions, ...definitions], [...stack2.observers, ...observers]),
query,
observe: (name, callback) => DefinitionStack(definitions, [{ name, callback }, ...observers])
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
NodeError = (chain, error) => ({chain, error, __is_an_error: true})
Insert cell
Insert cell
compact = sys => {
return html`<div>${
[...sys.definitions].reverse().map(
({name, fn, args}, i) => fn == VIEW ?
html`<div style="background-color: ${i % 2 == 1 ? 'beige' : 'default'}">
<span><span style="font-weight: bold">${name}</span>
= view of ${args[0]} with inputs (${args.slice(1).join(', ')})
</span></div>` :
html`<div style="background-color: ${i % 2 == 1 ? 'beige' : 'default'}">
<span><span style="font-weight: bold">${name}</span>
= ${JSON.stringify(fn, (k, v) => typeof v === 'function' ? v.toString() : v, 2)}
&nbsp; &nbsp; ( ${args.map((arg, j) => arg + ' = ' + (fn === VIEW ? '{<>}' :
JSON.stringify(sys.query(arg),
(k, v) => typeof v === 'function' ? `[fn]` : v, 2)))
.join(' , ')} )
</span><span style="float:right">${
JSON.stringify(sys.query(name), (k, v) => typeof v === 'function' ? `[[fn]]` : v, 2)}</span></div>`)}</div>`
}
Insert cell
stripes = sys => {
return html`<div>${[...sys.definitions].reverse().map(({name, fn, args}, i) => html`<div><div style="background-color: beige">${name} = ${JSON.stringify(fn, (k, v) => typeof v === 'function' ? v.toString() : v)} [[${args.map(arg => arg + '=' + JSON.stringify(sys.query(arg), (k, v) => typeof v === 'function' ? `[[fn]]` : v)).join(', ')}]]</div><div >${JSON.stringify(sys.query(name), (k, v) => typeof v === 'function' ? `[[fn]]` : v)}</div></div>`)}</div>`
}
Insert cell
viz = sys => {
let node_name = sys_name => console.log(sys_name) || sys_name + " :: " + (typeof sys.definitions.find(e => e.name == sys_name).fn === 'function' ? sys.definitions.find(e => e.name == sys_name).fn + " :: " + sys.query(sys_name) : sys.query(sys_name))
return dot`digraph test_mouseover_label {
${sys.definitions.map(({name, fn, args}) =>
`"${node_name(name)}" [label="${fn == VIEW ? `${name} :: VIEW of ${args[0]} with removed inputs ( ${args.slice(1).map(arg => arg).join(', ')} )` : node_name(name)}"] ; ` + args.map(arg => `"${node_name(arg)}" -> "${node_name(name)}"` ).join('; ') + (sys.definitions.find(e => e.name === fn) ? `"${node_name(fn)}" -> "${node_name(name)}"` : '') ).join('\n')}
}`
}
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more