Dangerous experiments with subatomic particles.
In this lab experiment, we're seeking to enable linking from one part of the microstate graph to a completely separate part. This will allows us to do things, like share a store between multiple instances of a microstate. See https://beta.observablehq.com/@cowboyd/microstate-variables for a statement of the problem.
The approach is to make microstates pass around the entire atom in which they are participating every time, and then treat themselves not as a Type and a value, but rather as a Type and a pointer into the atom. That way, if a graph of microstates all share the same atom, then they can reference other microstates with only a Type and a path into that atom. Transitions on the linked microstate will happen at that location in the atom (wherever in the atom that location happens to be). Because all operations anywhere in the graph happen on the atom, then they will be immediately visible to operations.
Let's see some concrete examples to see what that means. We won't use any new functionality that this scheme enables, only existing functionality. That way we can see the difference in representation.
the example of an App
which contains a Popup
. The popup has a
counter that it uses to track internally how many times it has been shown.
class App {
popup = Popup;
}
class Popup {
isOpen = Boolean;
count = Number;
show() {
return this
.isOpen.set(true)
.count.increment();
}
}
In the old way, each microstate kept a reference to its very own value:
let app = create(App, {popup: { isOpen: false, count: 0}});
metaOf(app).value //=> {popup: { isOpen: false, count: 0}}
metaOf(app.popup).value //=> { isOpen: false, count: 0 };
metaOf(app.popup.count).value //=> 0;
In the new way, notice how the app
and app.popup
, and
app.popup.count
all share the exact same atom
let app = create(App, {popup: { isOpen: false, count: 0}});
metaOf(app).atom //=> {popup: { isOpen: false, count: 0}}
metaOf(app.popup).atom //=> {popup: { isOpen: false, count: 0}}
metaOf(app.popup.count).atom //=> {popup: { isOpen: false, count: 0}}
The way they achieve a unique value is to lookup their path within the atom.
valueOf(app.popup.count) //=> 0
That way, value lookup works as expected.
Before, every transition started effectively at its own root of the tree. (the transition context was a microstate of the type used created from scratch). Now, there is a problem of where to mark the boundary of a transition, or What is the return value of a transition.
As we can see from our class Popup
, there are a couple places
where even though the transition is on a substate, it returns an
instance of Popup
. Which type and path to return is known in this
experiment as "ownership". Every microstate has an owner, where the
owner is the Type
+ path
of the parent microstate in which it
appears, and the target for which transitions should resolve to.
A microstate can be its own owner.
As you can see, all of the objects you reach from the app
object
have the same owner
ownerOf(app) //> { Type: App, path: [] }
locationOf(app) //> { Type: App, path: [] }
ownerOf(app.popup) //=> { Type: App, path: [] }
locationOf(app.popup) //=> { Type: Popup, path: ['popup'] }
ownerOf(app.popup.count) //=> { Type: App, path: [] }
locationOf(app.popup.count) //=> { Type: Number, path: ['popup', 'count'] }
That means that any transition invoked by any of these objects will
result in an App
object that points to path []
in the resulting
atom.
By contrast, within the show()
transition of the popup, the owner
will have shifted down to be the Popup
. But notice that the location
is still an absolute path within the total atom and not a relative path.
show() {
ownerOf(this) //=> { Type: Popup, path: ['popup']}
locationOf(this) //=> { Type: Popup, path: ['popup']}
ownerOf(this.isOpen) //=> { Type: Popup, path: ['popup']}
locationOf(this.isOpen) //=> { Type: Boolean, path: ['popup', 'isOpen']}
ownerOf(this.count) //=> { Type: Popup, path: ['popup']}
locationOf(this.count) //=> { Type: Number, path: ['popup', count']}
return this
.isOpen.set(true)
.count.increment();
}
Since a microstate can point to anywhere in the atom, and it can be owned by any microstate, this lets us embed any microstate pointing to anywhere that can be owned by anything. So, for example, imagine we have as part of our application a list of layers for a popup that lived at the path ['rendering', 'stack', 'layers']. We could create a popup that linked to this location even though the ['popup'] is not a descendant of this path.
Now, we can have a layers
property that is ownned by the popup,
but is located outside:
class Popup {
show() {
ownerOf(this.layers) //=> { Type: Popup, path: ['popup'] }
locationOf(this.layers) //=> { Type: LayerStack, path: ['rendering', 'stack', 'layers']}
return this
.layers.allocate()
.isOpen.set(true)
.count.increment();
}
}
The mechanism to declare this is still a work in progress, but the
test cases in lab.test.js
have a proof of concept in which one hand
claps the other.
// clap transition on hand look like this. It claps itself, and then
// claps the other. (other will be a link to the other hand)
clap() {
return this
.other.claps.increment()
.claps.increment()
}
// a person has a left hand and a right hand:
let person = create(Person, { left: { claps: 1 }, right: { claps: 1 } });
let clapped = person.right.clap();
// we clapped the right hand, but the left count incremented too.
clapped.left.claps.state //=> 2
Using the ability to "link into" a data structure anywhere inside the atom, we can use this to implement one of the holy grails of data structures: a Table
The cells are just a lazy enumeration that returns the cell type of the table. The rows are a lazy enumeration of rows, that lazily enumerate cells of the same type within the table. And, not suprisingly, the columns are a lazy enumeration of column objects that lazily enumerate the cells in that column.
See the testcases for example usage.