@webqit/quantum-js
v4.5.26
Published
Runtime extension to JavaScript that let's us do Imperative Reactive Programming (IRP) in the very language.
Downloads
114
Maintainers
Readme
Quantum JS
Overview • Creating Quantum Programs • Implementation • Examples • License
Quantum JS is a runtime extension to JavaScript that brings Imperative Reactive Programming to JavaScript!
What's that?
Overview
Where you normally would require certain reactive primitives to express reactive logic...
// Import reactive primitives
import { createSignal, createMemo, createEffect } from 'solid-js';
// Declare values
const [ count, setCount ] = createSignal(5);
const doubleCount = createMemo(() => count() * 2);
// Log this value live
createEffect(() => {
console.log(doubleCount());
});
// Setup periodic updates
setInterval(() => setCount(10), 1000);
Quantum JS lets you acheive the same in the ordinary imperative form of the language:
// Declare values
let count = 5;
let doubleCount = count * 2;
// Log this value live
console.log(doubleCount);
// Setup periodic updates
setInterval(() => count = 10, 1000);
Wanna play?:
- Add the following script to your page:
<script src="https://unpkg.com/@webqit/oohtml/dist/main.js"></script>
- Write your logic within a
quantum
script tag:
<script quantum>
// Declare values
let count = 5;
let doubleCount = count * 2;
// Log this value live
console.log(doubleCount);
// Setup periodic updates
setInterval(() => count = 10, 1000);
</script>
- Watch your console. Have fun.
Wanna see how magical things really are here? Update your step 2 to split the logic into two separate scripts:
- One ordinary script and one quantum script:
<script> // An ordinary script; no probelm
// Declare values
let count = 5;
let doubleCount = count * 2;
// Setup periodic updates
setInterval(() => count = 10, 1000);
</script>
<script quantum> // A quantum script; for live mode
// Log this value live
console.log(doubleCount);
</script>
Watching your console? Reactivity is still intact!
That reactivity is really happening within the Quantum script! It's a regular script in every way except that any peice of code thrown in is able to statically reflect changes to state in granular details!
To define, Quantum programs are an extension to JavaScript that has any piece of code stay sensitive to changes in the most fine-grained details - and entirely with no moving parts!
Now, while that is the <script quantum></script>
part of the HTML page above, there are many different ways to have this form of reactivity play out. Examples are just ahead.
This project pursues a futuristic, more efficient way to write reactive applocations today! And it occupies a new category in the reactivity spectrum!
Implementation
As seen above, you can have all of Quantum JS live in the browser!
Up there, we've used a version of the Quantum JS implementation that supports HTML <script>
elements. That is the OOHTML polyfill (OOHTML) and it's the most direct way to use Quantum JS in your web app.
Below are the standard Quantum JS polyfills - for when you aren't writing HTML <script>
elements. The design is such that you can easily use these as part of your tooling or compile process; e.g. to have Quantum JS as compile target.
<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!
// Destructure from the webqit namespace
const { QuantumFunction, AsyncQuantumFunction, QuantumScript, QuantumModule, State, Observer } = window.webqit;
// npm install
npm i @webqit/quantum-js
// Import API
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.
<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!
// Destructure from the webqit namespace
const { AsyncQuantumFunction, AsyncQuantumScript, QuantumModule, State, Observer } = window.webqit;
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> <!-- Must come before the polyfil --> <script src="https://unpkg.com/@webqit/quantum-js/dist/main.lite.js"></script> </head>
// npm install
npm i @webqit/quantum-js
// Import Lite API
import { AsyncQuantumFunction, AsyncQuantumScript, QuantumModule, State, Observer } from '@webqit/quantum-js/lite';
Creating Quantum Programs
You can write Quantum programs in either of two ways: as "Quantum Functions" and as "Quantum Scripts"!
Quantum Functions
Here, we introduce a new type of function that works like any other JavaScript function but in reactive mode. This function may be wrtten in any of the forms below:
Syntax 1: Using the quantum
Function keyword
Available since v4.3.
Here you prepend your function with the quantum
keyword, just in how you use the async
keyword:
// Quantum function declaration
quantum function bar() {
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
}
bar();
// Async quantum function declaration
async quantum function bar() {
let count = await 5;
let doubleCount = count * 2;
console.log(doubleCount);
}
await bar();
// Quantum function expression
const bar = quantum function() {
}
const bar = async quantum function() {
}
// Quantum object property
const foo = {
bar: quantum function() { ... },
}
const foo = {
bar: async quantum function() { ... },
}
// Quantum object method
const foo = {
quantum bar() { ... },
}
const foo = {
async quantum bar() { ... },
}
// Quantum class method
class Foo {
quantum bar() { ... }
}
class Foo {
async quantum bar() { ... }
}
// Quantum arrow function expression
const bar = quantum () => {
}
const bar = async quantum () => {
}
// Quantum arrow function expression
const bar = quantum arg => {
}
const bar = async quantum arg => {
}
This syntax is universally supported both within:
Quantum JS' dynamic APIs:
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();
and inline
<script>
elements (as made possible by the OOHTML Polyfill):<script> // 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 </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:
// Quantum function declaration
function** bar() {
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
}
bar();
// Async quantum function declaration
async function** bar() {
let count = await 5;
let doubleCount = count * 2;
console.log(doubleCount);
}
await bar();
// Quantum function expression
const bar = function** () {
}
const bar = async function** () {
}
// Quantum object property
const foo = {
bar: function** () { ... },
}
const foo = {
bar: async function** () { ... },
}
// Quantum object method
const foo = {
**bar() { ... },
}
const foo = {
async **bar() { ... },
}
// Quantum class method, optionally async
class Foo {
**bar() { ... }
}
class Foo {
async **bar() { ... }
}
This syntax is universally supported both within:
Quantum JS' dynamic APIs:
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();
and inline
<script>
elements (as made possible by the OOHTML Polyfill):<script> // 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 </script>
Syntax 3: Using Quantum Function Constructors
Here you use special function constructors to create a new Quantum function:
// Quantum function constructor
const bar = QuantumFunction(`
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
`);
bar();
// Async quantum function constructor
const bar = AsyncQuantumFunction(`
let count = await 5;
let doubleCount = count * 2;
console.log(doubleCount);
`);
await bar();
// With function parameters
const bar = QuantumFunction( param1, ... paramN, functionBody );
// With the new keyword
const bar = new QuantumFunction( param1, ... paramN, functionBody );
// As class property
class Foo {
bar = QuantumFunction( param1, ... paramN, functionBody );
}
This is how you obtain the APIs:
// Import API
import { QuantumFunction, AsyncQuantumFunction } from '@webqit/quantum-js';
const { QuantumFunction, AsyncQuantumFunction } = window.webqit;
Here, the difference between QuantumFunction
and AsyncQuantumFunction
is same as the difference between a regular synchronous JS function (function() {}
) and its async equivalent (async function() {}
).
// External dependency
globalThis.externalVar = 10;
// QuantumFunction
const sum = QuantumFunction(`a`, `b`, `
return a + b + externalVar;
`);
const state = sum(10, 10);
// Inspect
console.log(state.value); // 30
Note that, unlike the main Quantum JS build, the Quantum JS Lite edition only implements the
AsyncQuantumFunction
API which falls within the asynchronous operation of the Lite edition.
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'; // Equivalent to globalThis.c = 'c' assuming that we aren't running in a function scope or module scope
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:
// Quantum regular JS
const program = new QuantumScript(`
let count = 5;
let doubleCount = count * 2;
console.log(doubleCount);
`);
program.execute();
// Quantum module
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!
// Quantum module
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;
`);
This is how you obtain the APIs:
// Import API
import { QuantumScript, QuantumModule, AsyncQuantumScript } from '@webqit/quantum-js';
const { QuantumScript, QuantumModule, AsyncQuantumScript } = window.webqit;
Here, the difference between QuantumScript
, AsyncQuantumScript
, and QuantumModule
is same as the difference between a regular synchronous script element (<script>
), its async equivalent (<script async>
), and a module script (<script type="module">
).
Note that, unlike the main Quantum JS build, the Quantum JS Lite edition only implements the
AsyncQuantumScript
andQuantumModule
APIs which match the asynchronous nature of off the main thread compilation.
Consuming Quantum Programs
Each call to a Quantum function or script returns back a State
object that lets you 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); // 9
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); // 0
Now, the general-purpose, object-observability API: Observer API puts those changes right in your hands:
Observer.observe(state, 'value', mutation => {
//console.log(state.value); Or:
console.log(mutation.value); // 1, 2, 3, 4, etc.
});
Module Exports
For module programs, the State
object also features an exports
property that exposes the module's exports:
// Quantum module
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); // { module1, module2, module3, ..., localVar }
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:
// As module
const program = new QuantumModule(`
export let localVar = 0;
...
setInterval(() => localVar++, 500);
`);
const state = await program.execute();
console.log(state.exports); // { localVar }
Now, again, the Observer API puts those changes right in your hands:
Observer.observe(state.exports, 'localVar', mutation => {
//console.log(state.exports.localVar); Or:
console.log(mutation.value); // 1, 2, 3, 4, etc.
});
// Observe "any" export
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;
console.log('Total:', b);
}
bar();
But as an extension to regular JavaScript, Quantum programs maintain a live relationship with the outside world! This means that:
...Updates Happening On the Outside Are Automatically Reflected
Given the code above, the following will now be reflected:
// Update external dependency
a = 4;
The above dependency and reactivity hold the same even 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;
console.log('Total:', b);
}
bar();
And we get the same automatic dependency tracking with object properties:
// External value
const obj = { a: 2, b: 0 };
function** bar() {
obj.b = obj.a * 2;
console.log('Total:', obj.b);
}
bar();
// Update external dependency
obj.a = 4;
...Updates Happening On the Inside Are Observable
Given the same data binding principles, we are able to observe updates the other way round as to the updates made from the inside of the function: b = 4
, obj.b = 4
!
For updates to object properties, we're able to use the Observer API directly:
// Observe changes to object properties
const obj = { a: 2, b: 0 };
Observer.observe(obj, 'b', mutation => {
console.log('New value:', mutation.value);
});
The above holds the same also for global variables:
// Observe changes to global variables
b = 0; // globalThis.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 local variables 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 itself to achieve the exact:
(function** () {
console.log('New value:', b);
})();
...and, where necessary, we could next map those changes to an object to use the Observer API there:
(function** () {
obj.b = b;
})();
Observer.observe(obj, 'b', () => { ... });
Detailed Documentation
In how Quantum programs are based on literal JavaScript, no special syntaxes exist around here! And the information covered here is entirely optional. (But you may find it interesting to deep dive.)
Examples
Using the Quantum JS and Observer API polyfills, the following examples work today. While we demonstrate the most basic forms of these scenarios, it takes roughly the same principles to build more intricate equivalents.
Manual reactivity accounts for a large part of the UI code we write today. This time, though, we're able to 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() {
// Initial rendering
this._state = this.render();
// Static reflect at click time
this.addEventListener('click', () => {
this.count++;
//Observer.set(this, 'count', this.count + 1);
});
}
disconnectCallback() {
// Cleanup
this._state.dispose();
}
// Using the QuantumFunction constructor
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;
`);
});
Even outside of UI code, we often still need to write reactive logic! This time, though, we're able to 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) {
// The raw url
this.href = href;
// Initial computations
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:'; //Observer.set(url, 'protocol', 'http:');
console.log(url.href); // http://www.example.com/path
url.hostname = 'foo.dev'; //Observer.set(url, 'hostname', 'foo.dev');
console.log(url.href); // http://foo.dev/path
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.