Skip to content
Oxford Harrison edited this page Mar 3, 2025 · 121 revisions

Inside a Quantum Program (How It Works!)

This is documentation for Quantum JS v3.x. (Looking for v2.x?)

Reactivity

Imperative programs are a set of instructions that "act" on program state - with some of those instructions declaring said state, some modifying, and some referencing/consuming said state!

Examples:

// Instructions declaring state
let amount = 0;
// Instructions modifying state
amount = 10;
// Instructions referencing/consuming state
console.log(amount);

It is on this concept of state and instructions, and their interaction, that reactivity works in Quantum programs!

Here, each expression (or, technically, instruction) that depends on certain state (as with the console.log() expression above) implicitly tracks said state, and any update to said state causes it to re-run.

This is what happens in each case below:

// "var1" and "var2" being the references to state in this case
// and "var3" being a new state declaration after execution
let var3 = var1 + var2;
// "var1" and "var2" being the references to state in this case
console.log(var1, var2);
// "var1" and "var2" being the references to state in this case
// and "var3" having state update after execution
var3 = new Component(var1, var2);
// "var1" and "var2" being the references to state in this case
// and "var1.prop" and "var2.prop" having state update after execution
var1.prop = 2;
delete var2.prop;
// "var1" and "var2" being the references to state in this case
// and "var3" having state update after execution
var3 = { prop1: var1, [var2]: 0 };

...with Control Structures

For control structures - conditionals and loops - reactivity is based on references made at the top-level area of the construct:

// "testExpr" being the reference to state in this case
if (testExpr) {
  // Consequent branch
} else {
  // Alternate branch
}
// "testExpr", "case1" and "case2" being the references to state in this case
switch(testExpr) {
  case case1:
    // Branch 1
    break;
  case case2:
    // Branch 2
    break;
}

Above, referenced state is implicitly tracked and any update to said state causes the given construct to re-evaluate and the relevant branch of the construct ends up being re-run:

More examples
// "var1" and "var2" being the references to state in this case
if (var1 && var2) {
  // Consequent branch
} else {
  // Alternate branch
}
// "var1" and "var2" being the references to state in this case
switch(var1) {
  case 100:
    // Branch 1
    break;
  case var2:
    // Branch 2
    break;
}

In the case of loops, any update to referenced state causes the given loop to re-run:

// "testExpr" being the reference to state in this case
while(testExpr) {
  // Body
}
// "testExpr" being the reference to state in this case
do {
  // Body
} while(testExpr);
// "testExpr" being the reference to state in this case
for ( /*initExpr*/; testExpr; /*updateExpr*/) {
  // Body
}
// "iteratee" being the reference to state in this case
for (let val of iteratee) {
  // Body
}
// "iteratee" being the reference to state in this case
for (let key in iteratee) {
  // Body
}
More examples
// "var1" and "var2" being the references to state in this case
for (let count = var1; count < var2; count++) {
  // Body
}
// "var1" and "var2" being the references to state in this case
for (const val of [var1, var2]) {
  // Body
}
// "var1" being the reference to state in this case
for (const key in var1) {
  // Body
}

...with Nested Control Structures

Generally, the body of any control structure is an independent field of reactivity; every expression in there functions independently:

if (testExpr) {
  // Consequent branch
  // The following expression will react independently to its own observed changes
  console.log(var1);
} else {
  // Alternate branch
  // The following expression will react independently to its own observed changes
  console.log(var2);
}

Above, the nested console.log() expressions will track their own state and react to changes independently, but of course, depending on whether their containing branch is active or inert.

Nested control structures simply follow the same behaviour as above: tracking their own state and reacting to changes independently!

For example:

Being itself a statement nested within a block, the child "if" construct below will function independently - but this time, for as long as its containing branch is active:

// Parent construct
if (testExpr1) {
  // Consequent branch
} else {
  // Alternate branch
  // Child construct (which will react independently to its own observed changes)
  if (testExpr2) {
    // Consequent branch
  } else {
    // Alternate branch
  }
}
See equivalent layout
// Parent construct
if (testExpr1) {
  // Consequent branch
} else if (testExpr2) {
  // Consequent branch
} else {
  // Alternate branch
}

Sensitivity

Generally, reactivity happens when tracked state changes! For data structures like the below, tracking is depth-capable.

let document = { head: {}, body: {} };
console.log(document);
console.log(document.body);

Above, both expressions will track the referenced value, document, and will respond if replaced:

document = {};

but the second expression will also track the body property, and will respond if updated:

document.body = {};

And the same idea holds for deeper references.

...with Destructuring Assignments

JavaScript has exciting syntax sugars for working with data structures. For example, while we have written console.log(document.body) above, we could also write:

let { body } = document;
console.log(body);

Now reactivity should work the same in this case too!

But there's more: destructuring assignments could also take more complex forms, like:

let { prop1, prop2, prop3: prop3Alias } = object;

console.log(prop1);
console.log(prop2);
console.log(prop3Alias);

This time, reactivity should not only work the same but also retain its granularity; i.e. a single property change should be reflected independently of the rest!

Thus, in Quantum JS, the above is as reactively granular as its standalone equivalent below:

let prop1 = object.prop1;
let prop2 = object.prop2;
let prop3Alias = object.prop3;

console.log(prop1);
console.log(prop2);
console.log(prop3Alias);

...with Rest Assignments

Extending the previous, the runtime correctly understands the intent with rest assignments and correctly offers the same level of granularity as with their regular syntax equivalents.

Below, an update to object.prop3 is still independently reflected by the indirect target variable prop3Alias:

let { prop1, ...rest } = object;
let prop2 = rest.prop2;
let prop3Alias = rest.prop3;

console.log(prop1);
console.log(prop2);
console.log(prop3Alias);

Actually, the rest variable is held "live", such that as object eventually gets new properties, rest also has same, such that we as well have reactivity in tact with something like:

console.log(rest.prop4);
object.prop4 = 'Tada';
// Reflected above

...with Iterators

By default, and as seen earlier, the for ... of and for ... in iterators will re-run from the beginning on an update to iteratee:

for (let value of iteratee) {
  // Body
}

But they are also additionaly sensitive to in-place additions and removals on given iteratee and will incrementally reflect those changes without restarting! This lets us have "live" lists:

let items = [ 'one', 'two', 'three' ];
for (let item of items) {
  console.log(item);
}
items.push('four');
items.push('five', 'six');

Above, subsequent additions to items are reflected incrementally without restarting the loop:

And any deletions from `items` will also automatically be reflected

Here, the corresponding "round" in the loop is "deactivated", freeing up any relationships that may have been made on that round. Meanwhile any expression in the deactivated round that may have referenced the now undefined variable item (in this case, the console.log() expression) would have been triggered with undefined before the round dies. This gives us an opportunity to remove associated DOM elements, for example.

Examples

Here are rudimentary live lists examples wherein every additions and deletions are automatically reflected in the UI.

For for ... of loops, we detect deletions by testing item === undefined!

let items = ['one', 'two', 'three'];
for (let item of items) {
  let liElement = document.createElement('li');
  if (item === undefined) {
    liElement.remove();
    continue;
  }
  liElement.innerHTML = item;
  ulElement.append(liElement);
}
// Mutate items
items.push('four', 'five');
items.pop();
In practice...

...since the Observer API isn't yet native, the above mutations would need to happen via the Observer API:

Observer.proxy(items).push('four', 'five');
Observer.proxy(items).pop();

And for for ... in loops, we detect deletions by testing key in object!

let object = { one: 'one', two: 'two', three: 'three' };
for (let key in object) {
  let liElement = document.createElement('li');
  if (!(key in object)) {
    liElement.remove();
    continue;
  }
  liElement.innerHTML = key;
  ulElement.append(liElement);
}
object.four = 'four';
object.five = 'five';
delete object.five;
In practice...

...since the Observer API isn't yet native, the above mutations would need to happen via the Observer API:

Observer.set(object, 'four', 'four');
Observer.set(object, 'five', 'five');
Observer.deleteProperty(object, 'five');

Or alternatively:

const $object = Observer.proxy(object);
$object.four = 'four';
$object.five = 'five';
delete $object.five;

Update Model

Unlike traditional reactive systems that follow an event-based update model, Quantum programs follow a linear, control-flow-based update model!

In the former, updates are broadcast as events to arbitrary listeners:

import { createSignal, createEffect } from 'solid-js';

// count
const [ operand1, setOperand1 ] = createSignal(5);
const [ operand2, setOperand2 ] = createSignal(5);
const [ operand3, setOperand3 ] = createSignal(5);

// Callback location 1
createEffect(() => {
  console.log(operand1(), operand2(), operand3());
  // (5, 5, 5) <---- on the initial run
  // (5, 10, 5) <--- on the update below
});

// Update operand2
setOperand2(10); // Re-invokes callback location 1

// Callback location 2
createEffect(() => {
  console.log(operand1(), operand2(), operand3());
  // (5, 10, 5) <----- given the update above
});

But in contrast, in the equivalent Quantum program below, the update expression in the middle of the flow simply relies on the ongoing flow and ends up reflected at just the dependent expression downstream! And that upholds the behaviour of imperative programs:

let operand1 = 5;
let operand2 = 5;
let operand3 = 5;

console.log(operand1, operand2, operand3);
// (5, 5, 5) <----- on the initial flow

// Update operand2
operand2 = 10; // Point of change

// Reflection
console.log(operand1, operand2, operand3);
// (5, 10, 5) <----- given the update above

But should the update be made from outside the given flow, i.e. by an external event, then we have "reactivity"!

Here, updates trigger an artifical program flow involving just the dependents, and effective from the point of change:

let operand1 = 5;
let operand2 = 5;
let operand3 = 5;

// Reflection
console.log(operand1, operand2, operand3);
// (5, 5, 5) <----- on the initial flow
// (5, 10, 5) <---- on the external update below

// External event
setTimeout(() => {
  // Update operand2
  operand2 = 10;
}, 500);

// Reflection
console.log(operand1, operand2, operand3);
// (5, 5, 5) <----- on the initial flow
// (5, 10, 5) <---- on the external update above

This is to say: an update model that follows the same linear, deterministic flow of imperative programs!

But, there's also a cursor-level precision that happens with said flow at each dependent expression or statement! Here, the actual re-evaluation of the given dependent expressin or statement begins from the exact point of reference within said expression or statement!

For example, at each console.log() expression above, the actual re-evaluation begins from just after the middle argument:

console.log(operand1, operand2, operand3);
                              --->

Then it ends with the function call itself being made:

          -<------------------------------
console.log(operand1, operand2, operand3); |
            (5),      (10),   -(5)--------

Now, only the operand3 reference was ever looked up again, because, being the target, operand2 statically received the value 10, and being pre the target, operand1 was never visited, but had its value come from memo!

The significance of this level of precision becomes clearer especially where there's a cost to the resolution of said arguments:

Code
let operand1 = () => {
  rebuildTheWorld();
  console.log('Reading operand1');
  return 5;
};

let operand2 = {
  _value: 5,
  get value() {
    rebuildTheWorld();
    console.log('Reading operand2');
    return this._value;
  },
  set value(val) {
    console.log('Setting operand2');
    this._value = val;
  },
};

let operand3 = () => {
  rebuildTheWorld();
  console.log('Reading operand3');
  return 5;
};
console.log(operand1(), operand2.value, operand3());

Now, the update operation below should tell us which arguments are being re-evaluated:

operand2.value = 10;
Console
Setting operand2
Reading operand3

This is to say: a cursor-level precision that does no less and no more of what's needed to reflect an update! (The truest definition of "fine-grained", given its zero unnecessary recalculations/re-evaluations!)

Polyfill limitation here

This inline precision is yet to be attained by the current polyfill! But this is coming in the next major update!

...with Concurrency

The Observer API allows us to make batched updates, and Quantum programs are able to handle these updates in one event loop (the event loop within Quantum programs)! The significance of this can be seen in the demo below:

let object = {
    prop1: 1,
    prop2: 2,
};
function** program() {
  console.log(object.prop1);
  console.log(object.prop2);
  console.log(object.prop1, object.prop2);
}
program();

Here, we, of course, get the following console output on the initial run:

1
2
1, 2

Now, if we updated prop1 and prop2 in two separate events:

Observer.set(object, 'prop1', 10);
Observer.set(object, 'prop2', 20);

then we get the following console output:

10
10, 2
20
10, 20

But, if we updated prop1 and prop2 in one batch:

Observer.set(object, {
  prop2: 20,
  prop1: 10,
});

then, we get:

10
20
10, 20

Here, concurrency doesn't only ensure one linear flow for multiple updates, it also helps us optimise update performance!

Good a thing, this concurrency support doesn't involve any microtask scheduling - wherein the paradign entirely changes from synchronous to asynchronous programming. Everything happens synchronously and let's you work asynchronously at will.

Flow Control

Quantum programs support everything JavaScript gives us for plotting program flow; i.e. the various control structures we have: conditionals and loops - with which, respectively, we get the flow of execution to either branch conditionally or loop continuously until a certain condition is met! And with reactivity in the picture, as seen above, we are able to do more: get the program to automatically take a different execution path as state changes invalidate the conditions for the initial execution path!

For example, in the "if" construct below, we ordinarily would need to re-invoke program() in whole, in order to have a different execution path for when the condition (condition.value) is flipped:

const condition = { value: true };
function program() {
  buildTheWorld();
  if (condition.value) {
    console.log('Execution path: "consequent" branch.');
  } else {
    console.log('Execution path: "alternate" branch.');
  }
}
program();
setTimeout(() => condition.value = false);

But that's potentially tons of manual work and overheads dramatically cut with a little dose of reactivity! And that's Quantum programs automatically managing their own flow for us!

Yet, the story of "state-sensitive" control flow wouldn't be complete without the other flow control mechanisms coming in: the return, continue, and break statements!

...with the Return Statement

The return statement is an instruction to terminate execution within a running function and get control passed, optionally along with data, back to its caller. But while this spells the end of the story for regular functions, this is only another "instruction" for Quantum functions - on which we could also have reactivity! It's logical: state may still, for some valid reasons, change within a function post-return; and in that case...

  1. State changes may invalidate the initial return value:

    function program() {
      let returnValue = 0;
      setInterval(() => returnValue++);
      return returnValue;
    }
    let returnValue = program();

    and accounting for such changes (i.e. designing for "streaming returns") would ordinarily require a callback-based communication model:

    Code
    function program(callback) {
      let returnValue = 0;
      setInterval(() => {
        returnValue++;
        callback(returnValue);
      });
    }
    program((returnValue) => {
      // Handle
    });
  2. For "early returns", state changes may invalidate the conditions for the given execution path:

    const condition = { value: true };
    function program() {
      if (condition.value) {
        return 'Execution path with "early return".';
      }
      ...more code downstream
      return 'Execution path with "normal return".';
    }
    program();
    setTimeout(() => condition.value = false);

    and as with control structures generally, we ordinarily would need to re-invoke program() in whole, in order to have a different execution path!

Quantum programs would simply statically reflect those changes in each case!

  1. For "return values", the return instruction in Quantum programs is sensitive to changes on its given value and gets any subsequent update reflected on the program's output:

    function** program() {
      let returnValue = 0;
      setInterval(() => returnValue++);
      return returnValue;
    }
    let state = program();
    // Log initial return value
    console.log(state.value);
    // Log subsequent return value
    Observer.observe(state, 'value', e => {
      console.log(e.value);
    });

    and we could go without the Observer.observe() part when working with state.value (being a "live" property) from within a Quantum program itself:

    Code
    (function** () {
      let state = program();
      // Log return value always
      console.log(state.value);
    })();
  2. For "early returns", the idea remains the normal control flow behaviour in Quantum programs: automatically taking a different execution path as state changes invalidate the conditions for the initial execution path!

    Thus, to whatever extent that the situation calls for "early returns", e.g. in flattening "if/else" constructs, game on! you get the same behaviour as when you use multiple nested "if/else" constructs!

    Here, when the conditions for an "early return" changes, control will return to where it left off and proceed as normal with the code downstream:

    if (!condition1) {
      return 'Condition 1 not met';
    }
    ...more code downstream
    if (!condition2) {
      return 'Condition 2 not met';
    }
    ...more code downstream
    if (condition3) {
      if (!condition3_1) {
        return 'Condition 3 met, but not 3.1';
      }
      // Now, conditions 1, 2, 3, 3.1 have all been met.
      // We can even now do streaming values
      let returnValue = 0;
      setInterval(() => returnValue++);
      return returnValue;
    }
    // Condition 3 wasn't even met
    doSomething();

    and the program's overall output - state.value - will continue to reflect the corresponding return value at any given point!

...with the Continue Statement

The continue statement is an instruction to bail out of the active "round" of a loop and advance to its next round, or where a label is used, to the next round of the parent loop identified by the label:

parent: for (const val of iteratee) {
  child: for (const key in val) {
    if (condition1) {
      continue; // or: continue child
    }
    ...more code downstream
    if (condition2) {
      continue parent;
    }
    ...more code downstream
  }
}

This is, in many ways, like "early returns", but scoped to the individual rounds of a loop! So, can we have equivalent behaviour here between "continue" statements and "early returns"? Yes!

Here, when the initial conditions for a given continue statement changes, control will return to where it left off and proceed as normal with the code downstream!

If the continue statement had initially moved control out of child loop entirely to parent loop, thus, abandoning all subsequent rounds of child loop, said resumption will proceed until actual completion of said child loop, thus spanning everything that was initially abandoned!

...with the Break Statement

The break statement is an instruction to terminate the immediate running loop, or where a label is used, the parent loop identified by the label:

parent: for (const val of iteratee) {
  child: for (const key in val) {
    if (condition1) {
      break; // or: break child
    }
    ...more code downstream
    if (condition2) {
      break parent;
    }
    ...more code downstream
  }
}

This is, in many ways, like "early returns", but scoped to loops! So, again, can we have equivalent behaviour here between "break" statements and "early returns"? Yes!

Here, when the initial conditions for breaking a loop changes, control will return to where it left off and proceed as normal with the code downstream, and, afterwards, with the remaining rounds of the loop!

Experimental Features

The following features let us do more with Quantum programs but are non-standard and may change in the future!

A Non-Standard params Option

Whereas, standard function constructors have no provision for some params option, Quantum JS APIs have a params option - passed as the "last" argument that is of type object:

const params = {};
const sum = QuantumFunction(`a`, `b`, `
  return a + b;
`, params);
const program = new QuantumModule(`
  doSomething();
`, params);

This feature currently has many niche use cases around Quantum JS.

A Non-Standard params.env Option

Using the params option, it is possible to pass a virtual environment (env) object into the program from which to resolve non-local variables, just before asking the global scope:

const params = {
  env: { c: 5 },
};
const sum = QuantumFunction(`a`, `b`, `
  return a + b + c;
`, params);
console.log(sum(5, 5)); // 15

If you passed an env object to a QuantumScript instance specifically, then that becomes the "variable declaration" scope:

const params = {
  env: { c: 5 },
};
const program = new QuantumScript(`
  var d = 5;
  var c = 10 // Error; already declared in env
`, params);
await program.execute();
console.log(params.env); // { c: 5, d: c }
And, if you passed the same `env` object to more than one QuantumScript instance, then variables across scripts are cross-referencable
const program1 = new QuantumScript(`
  var e = 5;
  var f = 10
`, params);
await program1.execute();
const program2 = new QuantumScript(`
  console.log(e); // 5
  var f = 20 // Error; already declared in env by a sibling script
`, params);
await program2.execute();

Hot Module System

With Quantum Modules, we can already have "live" module exports:

const program1 = new QuantumModule(`
  export let localVar = 0;
  setInterval(() => localVar++, 500);
`);
const state = await program1.execute();
Observer.observe(state.exports, 'localVar', mutation => {
  console.log(mutation.value); // 1, 2, 3, 4, etc.
});

Now, what if we could also have "live" module imports, such that reactivity is carried through the wire, as it were:

const program2 = new QuantumModule(`
  import { localVar } from '...';
  console.log(localVar); // 1, 2, 3, 4, etc.
`);
await program2.execute();

This is currently possible!

Here, the exporting script would need to be named (via params.exportNamespace) for reference purposes:

const params = {
  exportNamespace: '#module-1',
};
const program1 = new QuantumModule(`
  export let localVar = 0;
  setInterval(() => localVar++, 500);
`, params);
const state = await program1.execute();

And the importing script would reference that as import specifier:

const program2 = new QuantumModule(`
  import { localVar } from '#module-1';
  console.log(localVar); // 1, 2, 3, 4, etc.
`);
await program2.execute();

Here, the import resolution system looks up #module-1 first from an in-memory "hot" module registry and returns that if found, then, or otherwise, falls back to the normal import resolution system.

This feature is currently being explored at OOHTML for a document-scoped "hot" module system for Single Page Applications:

<html>
  <head>

    <!-- include the OOHTML polyfill here -->

  </head>
  <body>

    <!-- exporting module -->
    <script type="module" quantum id="module-1">
      export let localVar = 0;
      setInterval(() => localVar++, 500);
    </script>

    <main> <!-- <<- this could be the individual pages of an SPA, which goes in and out of the DOM -->
    
      <!-- importing module -->
      <script type="module" quantum>
        import { localVar } from '#module-1'; // the ID of the module script above
        console.log(localVar); // 1, 2, 3, 4, etc.
      </script>

    </main>

  </body>
</html>

The idea here is to have modules exported somewhere at a parent location in the tree for use at child locations! The parent-child model helps ensure that the implied exporting script is in the DOM as at when importing script initiates an import!

Also, note that when a script leaves the DOM, its exports also become unavailable in the "hot" module registry.