Quantum JS

Overview • Creating Quantum Programs • Implementation • Examples • License
Quantum JS is a runtime extension to JavaScript that enables us do Imperative Reactive Programming (IRP) in the very language! This project pursues a futuristic, more efficient way to build reactive applocations today!
Quantum JS occupies a new category in the reactivity landscape!
Overview
Whereas you currently need a couple primitives to express reactive logic...
import { createSignal, createMemo, createEffect } from 'solid-js';
const [ count, setCount ] = createSignal(5);
const doubleCount = createMemo(() => count() * 2);
createEffect(() => {
console.log(doubleCount());
});
setTimeout(() => setCount(10), 500);
Quantum JS lets you acheive the same in the ordinary imperative form of the language:
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
setTimeout(() => count = 10, 500);
This time, the code you write is able to statically reflect changes to state in fine-grained micro details, without needing you to manually model your dependency graphs or worry about how values propagate through your code!
Idea
Show
Imperative programs are really the foundation for "state", "effect" and much of what we try to model today at an abstract level using, sometimes, functional reactive primitives as above, and sometimes some other means to the same end. Now, that's really us re-implementing existing machine-level concepts that should be best left to the machine!
Learn more
Right in how the instructions in an imperative program "act" on data - from the assignment expression that sets or changes the data held in a local variable (count = 10
) to the delete
operator that mutates some object property (delete object.value
), to the "if" construct that determines the program's execution path based on a certain state - we can see all of "state" (data), "effect" (instructions that modify state/data), and control structures (instructions informed by state, in turn) at play!
But what we don't get with how this works naturally is having the said instructions stay sensitive to changes to the data they individually act on! (The runtime simply not maintaining that relationship!) And that's where the problem lies; where a whole new way of doing things becomes necessary - wherein we have to approach literal operations programmatically: setCount(10)
vs count = 10
.
If we could get the JS runtime to add "reactivity" to how it already works - i.e. having the very instructions stay sensitive to changes to the data they individually act on - we absolutely would be enabling reactive programming in the imperative form of the language and entirely unnecessitating the manual way!
This is what we're exploring with Quantum JS!
└ You may want to learn more in the introductory article: Re-Exploring Reactivity and Introducing the Observer API and Reflex Functions
Status
- Actively maintained
- A working implementation
- Integral to the OOHTML project
- Open to contributions
Implementation
Quantum JS may be used today. While this is a full-fledged compiler at heart, there is no compile step required, and you can have all of Quantum JS live in the browser!
Load from a CDN
└───────── 
<script src="https://unpkg.com/@webqit/quantum-js/dist/main.js"></script>
└ This is to be placed early on in the document and should be a classic script without any defer
or async
directives!
const { QuantumFunction, AsyncQuantumFunction, QuantumScript, QuantumModule, State, Observer } = window.webqit;
Install from NPM
└───────── 
npm i @webqit/quantum-js
import { QuantumFunction, AsyncQuantumFunction, QuantumScript, AsyncQuantumScript, QuantumModule, State, Observer } from '@webqit/quantum-js';
Quantum JS Lite
It is possible to use a lighter version of Quantum JS where you want something further feather weight for your initial application load.
Load from a CDN
└───────── 
<script src="https://unpkg.com/@webqit/quantum-js/dist/main.lite.js"></script>
└ This is to be placed early on in the document and should be a classic script without any defer
or async
directives!
const { AsyncQuantumFunction, AsyncQuantumScript, QuantumModule, State, Observer } = window.webqit;
Additional details
The Lite APIs initially come without the compiler and yet lets you work with Quantum JS ahead of that. Additionally, these APIs are able to do their compilation off the main thread by getting the Quantum JS compiler loaded into a Web Worker!
But if you may, the Quantum JS Compiler is all still loadable directly - as if short-circuiting the lazy-loading strategy of the Lite APIs:
<head>
<script src="https://unpkg.com/@webqit/quantum-js/dist/compiler.js"></script>
<script src="https://unpkg.com/@webqit/quantum-js/dist/main.lite.js"></script>
</head>
Install from NPM
└───────── 
npm i @webqit/quantum-js
import { AsyncQuantumFunction, AsyncQuantumScript, QuantumModule, State, Observer } from '@webqit/quantum-js/lite';
Creating Quantum Programs
This feature comes both as a new function type: "Quantum Functions" and as a new execution mode for whole programs: "Quantum Scripts"!
Given a language-level feature, no setup or build step is required!
Quantum Functions
You can make a Quantum function via either of three ways:
Syntax 1: Using the quantum
Function Flag
Available from v4.3.
Here you prepend your function with the quantum
flag, just in how you use the async
function flag:
quantum function bar() {
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
}
bar();
async quantum function bar() {
let count = await 5;
let doubleCount = count * 2;
console.log(doubleCount);
}
await bar();
Show more syntax examples
const bar = quantum function() {
}
const bar = async quantum function() {
}
const foo = {
bar: quantum function() { ... },
}
const foo = {
bar: async quantum function() { ... },
}
const foo = {
quantum bar() { ... },
}
const foo = {
async quantum bar() { ... },
}
class Foo {
quantum bar() { ... }
}
class Foo {
async quantum bar() { ... }
}
const bar = quantum () => {
}
const bar = async quantum () => {
}
const bar = quantum arg => {
}
const bar = async quantum arg => {
}
Show polyfill support
This syntax is supported from within any piece of code compiled by the Quantum JS compiler, e.g.:
-
code made via Quantum JS APIs (discussed below):
const program = new QuantumFunction(`
// External dependency
let externalVar = 10;
// QuantumFunction
quantum function sum(a, b) {
return a + b + externalVar;
}
const state = sum(10, 10);
// Inspect
console.log(state.value); // 30
`);
program();
-
code within inline <script>
tags when using the OOHTML Polyfill (discussed below):
<script>
let externalVar = 10;
quantum function sum(a, b) {
return a + b + externalVar;
}
const state = sum(10, 10);
console.log(state.value);
</script>
Syntax 2: Using the Double Star **
Notation
Here you append your function with the double star **
notation, much like how you write generator functions:
function** bar() {
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
}
bar();
async function** bar() {
let count = await 5;
let doubleCount = count * 2;
console.log(doubleCount);
}
await bar();
Show more syntax examples
const bar = function** () {
}
const bar = async function** () {
}
const foo = {
bar: function** () { ... },
}
const foo = {
bar: async function** () { ... },
}
const foo = {
**bar() { ... },
}
const foo = {
async **bar() { ... },
}
class Foo {
**bar() { ... }
}
class Foo {
async **bar() { ... }
}
Show polyfill support
This syntax is supported from within any piece of code compiled by the Quantum JS compiler, e.g.:
-
code made via Quantum JS APIs (discussed below):
const program = new QuantumFunction(`
// External dependency
let externalVar = 10;
// QuantumFunction
function** sum(a, b) {
return a + b + externalVar;
}
const state = sum(10, 10);
// Inspect
console.log(state.value); // 30
`);
program();
-
code within inline <script>
tags when using the OOHTML Polyfill (discussed below):
<script>
let externalVar = 10;
function** sum(a, b) {
return a + b + externalVar;
}
const state = sum(10, 10);
console.log(state.value);
</script>
Syntax 3: Using Quantum Function Constructors
Here you use special function constructors to create a new Quantum function:
const bar = QuantumFunction(`
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
`);
bar();
const bar = AsyncQuantumFunction(`
let count = await 5;
let doubleCount = count * 2;
console.log(doubleCount);
`);
await bar();
Show more syntax examples
const bar = QuantumFunction( param1, ... paramN, functionBody );
const bar = new QuantumFunction( param1, ... paramN, functionBody );
class Foo {
bar = QuantumFunction( param1, ... paramN, functionBody );
}
Show polyfill support
This is the direct syntax of the Quantum JS APIs:
import { QuantumFunction, AsyncQuantumFunction } from '@webqit/quantum-js';
const { QuantumFunction, AsyncQuantumFunction } = window.webqit;
API | Equivalent semantics... |
---|
QuantumFunction | function() {} |
AsyncQuantumFunction | async function() {} |
globalThis.externalVar = 10;
const sum = QuantumFunction(`a`, `b`, `
return a + b + externalVar;
`);
const state = sum(10, 10);
console.log(state.value);
Note that, unlike the main Quantum JS build, the Quantum JS Lite edition only implements the AsyncQuantumFunction
API which falls within the premise of off the main thread compilation.
Additional details
Note that unlike normal function declarations and expressions that can see their surrounding scope, as in syntaxes 1 and 2 above, code in function constructors is only able to see the global scope:
let a;
globalThis.b = 2;
var c = 'c';
const bar = QuantumFunction(`
console.log(typeof a); // undefined
console.log(typeof b); // number
console.log(typeof c); // string
`);
bar();
Quantum Scripts (Whole Programs)
Here, whole programs are able to run in quantum execution mode using special scripting APIs:
const program = new QuantumScript(`
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
`);
program.execute();
const program = new QuantumModule(`
let count = await 5;
let doubleCount = count * 2;
console.log(doubleCount);
`);
await program.execute();
These will run in the global scope!
The latter does certainly let you use import
and export
statements!
Exanple
const program = new QuantumModule(`
import module1, { module2 } from 'package-name';
import { module3 as alias } from 'package-name';
...
export * from 'package-name';
export let localVar = 0;
`);
Show polyfill support
This is the direct syntax of the Quantum JS APIs:
import { QuantumScript, QuantumModule, AsyncQuantumScript } from '@webqit/quantum-js';
const { QuantumScript, QuantumModule, AsyncQuantumScript } = window.webqit;
API | Equivalent semantics... |
---|
QuantumScript | <script> |
QuantumModule | <script type="module"> |
AsyncQuantumScript | <script async> |
Note that, unlike the main Quantum JS build, the Quantum JS Lite edition only implements the AsyncQuantumScript
and QuantumModule
APIs which falls within the premise of off the main thread compilation.
Now, this brings us to having real Quantum scripts in HTML:
<script quantum>
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
</script>
<script type="module" quantum>
let count = await 5;
let doubleCount = count * 2;
console.log(doubleCount);
</script>
But this as a HTML-level feature is made possible via a related project: OOHTML! You'll, this time, need to use the OOHTML polyfill, instead of the Quantum JS polyfill, to have Quantum HTML Scripts.
You're in fact able to also use the quantum
or double star **
function notation right within an ordinary imline script:
<script>
quantum function bar() {
}
</script>
That said, other tooling may choose to use the same API infrastructure in other ways; e.g. as compile target.
Consuming Quantum Programs
Each call to a Quantum function or script returns back a State
object that lets us consume the program from the outside. (This is similar to what generator functions do.)
For the Quantum functions above:
const state = bar();
For the Quantum Script APIs above:
const state = program.execute();
For Quantum HTML scripts - <script quantum>
, the state
object is available as a direct property of the script element:
console.log(script.state);
Return Value
The State
object features a value
property that carries the program's actual return value:
function** sum(a, b) {
return a + b;
}
const state = sum(5, 4);
console.log(state.value);
But given a "live" program, the state.value
property also comes as a "live" property that always reflects the program's new return value should anything make that change:
function** counter() {
let count = 0
setInterval(() => count++, 500);
return count;
}
const state = counter();
console.log(state.value);
Now, the general-purpose, object-observability API: Observer API puts those changes right in our hands:
Observer.observe(state, 'value', mutation => {
console.log(mutation.value);
});
Module Exports
For module programs, the State
object also features an exports
property that exposes the module's exports:
const program = new QuantumModule(`
import module1, { module2 } from 'package-name';
import { module3 as alias } from 'package-name';
...
export * from 'package-name';
export let localVar = 0;
`);
const state = await program.execute();
console.log(state.exports);
But given a "live" program, each property in the state.exports
object also comes as a "live" property that always reflects an export's new value should anything make that change:
const program = new QuantumModule(`
export let localVar = 0;
...
setInterval(() => localVar++, 500);
`);
const state = await program.execute();
console.log(state.exports);
Now, again, the Observer API puts those changes right in our hands:
Observer.observe(state.exports, 'localVar', mutation => {
console.log(mutation.value);
});
Observer.observe(state.exports, mutations => {
mutations.forEach(mutation => console.log(mutation.key, mutation.value));
});
Disposing Quantum Programs
Quantum programs may maintain many live relationships and should be disposed when their work is done! The State
object they return exposes a dispose()
method that lets us do just that:
state.dispose();
For Quantum HTML Scripts - <script quantum>
, state disposal is automatic as script element leaves the DOM!
Interaction with the Outside World
Quantum programs can read and write to the given scope in which they run; just in how a regular JavaScript function can reference outside variables and also make side effects:
let a = 2, b;
function** bar() {
b = a * 2;
}
bar();
But as an extension to regular JavaScript, Quantum programs maintain a live relationship with the outside world! This means that:
...Updates Happening from the Outside Are Automatically Reflected
Given the code above, the following will now be reflected:
a = 4;
The above holds the same if we had a
in the place of a parameter's default value:
let a = 2, b = 0;
function** bar(param = a) {
b = param * 2;
}
bar();
And we get the same automatic dependency tracking with objects:
const obj = { a: 2, b: 0 };
function** bar() {
obj.b = obj.a * 2;
}
bar();
obj.a = 4;
...Updates Happening from the Inside Are Observable
Given the same idea of automatic data binding, we are able to observe updates the other way around as in the updates made from the inside of our functions above: b = 4
, obj.b = 4
!
For updates to object properties, we're able to use the Observer API directly:
const obj = { a: 2, b: 0 };
Observer.observe(obj, 'b', mutation => {
console.log('New value:', mutation.value);
});
The above holds the same for global variables:
b = 0;
Observer.observe(globalThis, 'b', mutation => {
console.log('New value:', mutation.value);
});
And for updates to local variables, while we can't use the Observer API directly as these aren't associated with a physical object as we have of global variables...
let b = 0;
Observer.observe(?, 'b', () => { ... });
...we're able to use a Quantion function to achieve the exact:
(function** () {
console.log('New value:', b);
})();
...or we could map those changes to an object to use the Observer API there:
(function** () {
obj.b = b;
})();
Observer.observe(obj, 'b', () => { ... });
Inside a Quantum Program (How It Works!)
In how Quantum programs can already entirely manage themselves, knowledge of how they work is very much optional! But, if you may look, this section covers just that very awesome part!
Knowing how things work presents a great way to reason about Quantum programs, and a better background for taking full advantage of the "Quantum" magic to never again do manual work!
Examples
Using the Quantum JS and Observer API polyfills, the following examples work today. While we'll demonstrate the most basic forms of these scenarios, it takes roughly the same principles to build more intricate equivalents.
Example 1: Reactive Custom Elements
└─────────
Manual reactivity accounts for a large part of the UI code we write today. But, what if we could simply write "Quantum" logic?
In this example, we demonstrate a custom element that has a Quantum render()
method. We invoke the render()
method only once and let every subsequent prop change be statically reflected:
customElements.define('click-counter', class extends HTMLElement {
count = 10;
connectedCallback() {
this._state = this.render();
this.addEventListener('click', () => {
this.count++;
});
}
disconnectCallback() {
this._state.dispose();
}
render = QuantumFunction(`
let countElement = this.querySelector( '#count' );
countElement.innerHTML = this.count;
let doubleCount = this.count * 2;
let doubleCountElement = this.querySelector( '#double-count' );
doubleCountElement.innerHTML = doubleCount;
let quadCount = doubleCount * 2;
let quadCountElement = this.querySelector( '#quad-count' );
quadCountElement.innerHTML = quadCount;
`);
});
Example 2: Pure Computations
└─────────
Even outside of UI code, we often still need to write reactive logic! Now, what if we could simply write "Quantum" logic?
In this example, we demonstrate a simple way to implement something like the URL API - where you have many interdependent properties!
class MyURL {
constructor(href) {
this.href = href;
this.compute();
}
compute = QuantumFunction(`
// These will be re-computed from this.href always
let [ protocol, hostname, port, pathname, search, hash ] = new URL(this.href);
this.protocol = protocol;
this.hostname = hostname;
this.port = port;
this.pathname = pathname;
this.search = search;
this.hash = hash;
// These individual property assignments each depend on the previous
this.host = this.hostname + (this.port ? ':' + this.port : '');
this.origin = this.protocol + '//' + this.host;
let href = this.origin + this.pathname + this.search + this.hash;
if (href !== this.href) { // Prevent unnecessary update
this.href = href;
}
`);
}
└ Instantiate MyURL
:
const url = new MyURL('https://www.example.com/path');
└ Change a property and have it's dependents auto-update:
url.protocol = 'http:';
console.log(url.href);
url.hostname = 'foo.dev';
console.log(url.href);
Getting Involved
All forms of contributions are welcome at this time. For example, syntax and other implementation details are all up for discussion. Also, help is needed with more formal documentation. And here are specific links:
License
MIT.