Very much work in progress.
Relatively low level primitives are available to create reactive applications. There is basic JSX support for creating elements more easily and for combining elements.
(The following are code fragments from kagome-demo/src/demo.ts
. They are meant
to show what code using Kagome looks like, so they might not make sense. Check
the full demo file for details)
Import Kagome, using K
as the shorthand prefix:
import * as K from 'kagome';
Creating a process using K.process
:
const Interact:
(props?: {}) => K.Process<HTMLDivElement> =
() => K.process((run) => {
Processes must be pure, when disregarding calls to run
. Wrap impure things to
avoid recomputation: (In general, impurity must be used with caution.)
const id = run(() =>
K.pureS(`inp-${Math.random() * Math.pow(2, 52)}`)
);
Creating writable reactive values called registers:
const classR = run(() => K.reg<string | undefined>(undefined));
Creating derived read-only reactive values using combinators. f
means applying a function, and s
prefix means the function itself returns something reactive (s
for Sentinel
, basically another thing that can run
and returns a value).
const filteredS = run(() => valueR.f(x => x.trim()));
const correctS = run(() => filteredS.f(x => x === i.toString()));
const tooMuchS = run(() => filteredS.f(x =>
x.length - i.toString().length > 10))
const classS = run(() => correctS.f(val => val ? 'ok' : 'wrong'));
const promptS = run(() => filteredS.sf(val =>
val !== undefined && val !== '' && val !== i.toString()
? <p class="prompt">{filteredS} isn't right</p>
: K.pureS(null)
));
const extraS = run(() => tooMuchS.sf(val =>
val
? <p class="prompt">Forget about it</p>
: K.pureS(null)
));
Creating and composing elements and components using JSX syntax. Note how attributes are allowed to be reactive values, and how the reactive values are distributed among UI elements.
const part = run(() =>
<div>
<label for={id}>Please type {i}: </label>
<Input id={id} class={classS} valueR={valueR} hidden={tooMuchS} />
{promptS}
{extraS}
</div>
);
Input
is a wrapper component over HTML input
. Note how rest
passes through
HTML attributes:
const Input:
(props: { valueR: K.Register<string> }
& K.JSX.ElementProps<HTMLInputElement>)
=> K.Process<HTMLInputElement> =
({ valueR, ...rest }) => K.process((run) => {
const inp = run(() => <input {... rest} />) as HTMLInputElement;
run(() => K.domEvent(inp, 'input')(
() => valueR.setDirectly(inp.value)
));
return inp;
});
Running actions:
run(() => K.appendChildD(container, part));
So far, nothing out of the ordinary. This is about to change.
You can read a reactive register by running it:
const value = run(() => valueR);
You saw it right: You can work with reactive values without dealing with event handlers. You can literally just ask for its value.
Effectively, you can write your code only thinking in the forward direction, generating output from input, and Kagome will take care of switching between branches of history. For example:
if (value !== i.toString()) {
run(() => classR.setD('wrong'));
if (value === undefined || value === '') {
// Input is empty
run(() => hiddenR.setD(true));
}
break;
} else {
run(() => hiddenR.setD(true));
run(() => classR.setD('ok'));
}
Note that when the input value (valueR
) changes and value !== i.toString()
is still true, if the new value is not empty, hiddenR
will automatically
revert to the previous value. There is no need to handle this case explicitly.
Since a process does exactly what was needed to move from one history to the
next, there is no need for a virtual DOM. container
is a native
HTMLDivElement
and can be used elsewhere:
(The assertion is needed due to a limitation in TypeScript's JSX support.)
return container as HTMLDivElement;
});
The following shows some composition capabilities, using both JSX and plain JS
syntax together, and a combinator K.mapped
for running actions in parallel:
K.toplevel((run) => {
const app = run(() =>
<div class="main">
{<Interact />}
{K.mapped([Interact(), <Interact />])}
</div>
);
run(() => K.appendChildD(main, app));
});
As promised, Kagome is a framework for imperative reactive programming, and it works by tracking a history. The basics are as follows:
- A process runs from start to finish, without needing to care about how to update everything due to changes.
- The Kagome runtime tracks checkpoint objects (
Runnable
type). (Checkpoint objects are those returned by the thunk, which is the function passed torun
) - Each checkpoint object can either have a value and a trigger event, or have an
'undo' action, or have both.
- The value and trigger event usually means some dynamic value (Called
Sentinel
since it 'watches' something changing). Whenrun
the process listens to the trigger event. - An 'undo' action can correspond to destroying a resource (like
unregistering a listener) or literally undoing something (like
appendChild
).
- The value and trigger event usually means some dynamic value (Called
- When a
Sentinel
triggers, the whole process is unwound to the point after theSentinel
was sent torun
, the new value of theSentinel
is used, and execution restarts there.- Unwinding in this case means undoing every checkpoint object and
unlistening to
Sentinels
in reverse order until the desired position is reached.
- Unwinding in this case means undoing every checkpoint object and
unlistening to
But how is this time travel event handling possible, given that JavaScript has no advanced control flow features like continuations?
This is where the funny syntax of run(() => ...)
comes into play. Essentially,
when it is needed to restart a process from a certain point in the middle, we
instead restart it from the beginning. We count the number of calls to run
and
return cached results until we reach the desired number. That is why the
process needed to be pure. Specifically, all resource-creating actions must be
wrapped in run(() => K.pureS(...))
to avoid getting a new version every time.
S
meansSentinel
D
meansDisposable
R
meansRegister
Kagome Kagome (
かごめかごめ
, or籠目籠目
) is a Japanese children's game and the song associated with it. — Wikipedia: Kagome Kagome
The 'kagome' is chosen to mean 'caged bird' in this context. It is a reference to the word 'capturing' in the Capturing the Future by Replaying the Past technique (See Functional Pearl), which Kagome implements a part of. (Kagome's implementation is not based on the published one as the full feature set of delimited continuations is not required.)
Disclaimer: No bird has been caged or otherwise made to suffer in the making of this framework.