By admitting input, a program acquires a control language by which a user can guide the program through a maze of possibilities. — Chuck Moore
An approach to interactive apps/games I'd wanted to try in a toy language but realized is largely language-agnostic, and easy to explain to anybody who understands Redux/Elm. I had a week+ to prototype it in JS.
a proof-of-concept live coding JS environment, where user actions call WRITE(...)
to modify app source
This is a bad idea in many ways (⚠ including security!) but it challenges assumptions on essential complexity and walls between language/env authors | developer | end-user.
-
Browsers popularized what user sees being a pure function of DOM.
-
React popularized DOM being pure function of your data/state ("model").
-
We need a live coding environment where the code you edit gets re-evaluated on every edit. (TODO: to scale this needs caching of intermediate results, but full re-run works for a PoC).
-
Now add functions that take model and return new model.
-
If user were willing to type function calls into editor, you're done ;-) This is an internal DSL approach to interaction: Model + View is all you need, and you can be purely functional without any in-language approach to state (like in spreadsheets).
-
To make it friendlier, classical UI explicitly reads events, translates them to state changes — i.e. treats user input as external DSL. Don't do that!
Instead, translate user actions to changes in the source editor — which trigger re-computation. -
Traditional developer (especially one attempting event sourcing / time travel / record-replay live coding) needs two concepts of stateful change: changing state inside the app, but also changing the app source. This approach reduces it to only one.
-
Think of an event-sourcing DB migration changing the format of past events, or a refactor changing Redux actions structure, invalidating the recorded history. Fixing those requires thinking of both "change" concepts at once :-/
In this self-modify paradigm you get same issues — but history is regular code, so regular "refactor after a funciton interface changed" skills apply.
-
- https://model-view-self-modify.netlify.app/ (or open
index.html
locally, or serve it by e.g.python3 -m http.server
). - Paste the content of tetris.js into the editor.
- Start moving "TIME TRAVEL" line up.
- Put editor cursor before it and start clicking [left] [right] [down] buttons to play from that moment.
- Put cursor inside
RCSet([...])
innewGame.board
. Start clicking board cells to mark them occupied.
-
localStorage is awesome for edit/reload development! 2 lines for major quality of life improvement
-
JS without bundler/transpiler is fun again.
-
CDNs serving npm packages: unpkg.com, skypack.dev, esm.sh
- If you don't want CDNs, there is snowpack.dev (didn't try it)
-
Can use ESM imports inside
<script type="module">
- may also need a
<script type="importmap">
- may also need a
-
htm
library: JSX-like notation in JS tagged templates
-
-
JS eval() facilities are surprisingly junky :-( I spent most of the week tweaking my MVP environment instead of using it...
- literal
eval()
has magic access to current scope myEval = eval; myEval()
doesn't. It's considered "safer" because the scope it'll pollute is — reliably — only the GLOBAL scope :-Dnew Function()
constructor is better, separates parsing from running & clean passing of values.- SyntaxError within eval() / Function() doesn't report what line it happened (only Firefox does, non-standard) => KLUDGE: keep removing lines from the end until the error changes/goes away
//# sourceURL=
directive for cleaner stack traces- TODO: dynamically creating
<script>
tags looks promising?
- literal
-
It's not just for end-user interaction! WRITE() makes it easy to scaffold helpers you use while coding.
-
I hoped to build Light Table-like env that magically renders values under the cursor, i.e. time travel would be "stop evaluation at this point". I didn't get to that, but the twist is that's not good enough anyway — time travel requires stopping Model manipulation early, but still running later View code!
=> I experimented with a kludge: eval skipping editor selection. Unusual and causes lots of flicker.
=> Dropped that in favor of moving/*
...*/
markers – zero magic just regular editor shortcuts. -
JS syntax is hard to slice into safe-for-partial execution chunks. A nd
-
is React conceptual overkill here?
For the most part, instead ofhtml`<${View} ...${model}/>`
it's simpler to writeView(model)
.
The deep benefit of React.createElement is separation from "mount component now" vs. "render it later", and supporting a stateful lifecycle...
But if we [pretend to] re-evaluate everything all the time, we can just call functions (like in Elm!)- It does give opportunity for some caching.
- More importantly, it provides well-understood seam between stateless parts and off-the-shelf stateful-lifecycle parts.
-
finish the Tetris
-
look for max opportunities to use WRITE() during coding - "moldable development"
- "level editor" kind of stuff
- color picker
-
It's fragile to place editor cursor correctly before interaction. Add a way to target a fixed place in code.
- MVP:
BEFORE_COMMENT('FOO').WRITE(...)
targetting//FOO
or/*FOO*/
?
(It's important to avoid target the code itself that mentions the target name :-)
- MVP:
-
go meta: Shift parts of the live env e.g. into the env itself so they can be edited?
- serialize CodeMirror edit actions to a text stream, allow time travel there too?!
but more important:
-
computation caching! Don't re-run code from start, esp. when appending at the end.
- check out https://tomasp.net/academic/papers/live/
- mobx?
-
don't reinvent the env — build on observablehq or similar. (https://github.com/asg017/dataflow ?)
- Observable notebook already parses separate JS cells, computes data dependencies and manages re-computation...
-
Try React "fast refresh" API to replace re-defined components in-place?