Notes on let-signal evaluation

This commit is contained in:
Yura Dupyn 2026-02-13 19:00:08 +01:00
parent 182307a81f
commit c255e19c42
2 changed files with 53 additions and 89 deletions

View file

@ -1,86 +0,0 @@
```javascript
const Thing = (initState) => ({
state: initState,
subscribers: [],
set(transform) {
const state = transform(this.state);
this.state = state;
this.subscribers.forEach(f => {
f(state);
});
},
get() {
return this.state;
},
subscribe(f) {
this.subscribers.push(f);
},
map(transform) {
const Y = Thing(this.state);
this.subscribe(x => {
Y.set(() => transform(x));
});
return Y;
},
});
function pair(X, Y) {
const Z = Thing([X.get(), Y.get()]);
X.subscribe(x => {
Z.set(() => [x, Y.get()]);
});
Y.subscribe(y => {
Z.set(() => [X.get(), y]);
});
return Z;
}
// Comonad lift
// Signal(a), (Signal(a) -> b) -> Signal(b)
// X: Signal(A), f: Signal(A) -> B
// Y: Signal(B)
// function extend(X, f) {
// const y0 = f(X);
// const Y = Thing(y0);
// X.subscribe(x => {
// Y.set(f(???)); I need to somehow feed it a new signal...
// // TODO: Ofcourse I can feed it `X` again, but that feels wrong... I thought
// });
// return Y;
// }
const count = Thing(0);
console.log("COUNT EXISTS");
console.log(count.get())
count.subscribe(x => {
console.log("count is now", x);
});
count.set(() => 1)
count.set(() => 2)
const double = count.map(x => 2*x);
console.log("DOUBLE EXISTS");
double.subscribe(x => {
console.log("double is now", x);
});
count.set(() => 3);
count.set(() => 9);
const WTF = pair(count, double)
console.log("-> WTF EXISTS");
WTF.subscribe(([x, y]) => {
console.log("WTF is now ", [x, y]);
});
count.set(() => 13);
```

View file

@ -472,9 +472,7 @@ let-signal {
```
// === parametrised signals ===
# Parametrise Signals
```
// like a top-level function of type (A, B, C) -> Signal(D)
@ -483,3 +481,55 @@ fn-signal Foo(x1, x2, x3) {
}
```
# Implementation
## Signal Env/Frame/Binding
```
type SignalFrame = {
pattern: ProductPattern,
expr: Signal<Value>,
}
```
This is almost like a signal-env. Seems useful.
## Let-Signal binding
```
let-signal {
x := sig-expr-0,
y := sig-expr-1
. f(x, y)
}
```
What happens during the evaluation of the above signal-expression?
1. evaluate `(sig-expr-0, sig-expr-1)` to `(sig0, sig1)` and construct a signal-env
`[ x := sig0, y := sig1 ]`
2. evaluate `initVal := f(x, y) in env [ x := sig0.read(), y := sig1.read() ]`
3. construct new signal `Z := signal(initVal)`
4. make `Z` depend on `sig0` and `sig1`.
When one of them changes, push new value on `Z` that's the result of evaluation of
`f(x, y) in env [ x := sig0.read(), y := sig1.read() ]`
Note how `Z` is a signal together with a special closure:
- body of the closure is `f(x, y)`
- the captured signal-env of the closure is `[ x := sig0, y := sig1 ]`
- `Z` depends on `(sig0, sig1)`
TODO: Maybe it would be better to have something like signal-values?
These can either be plain constants,
or something more complex that has dependencies...
Right now everything is forced to be `Signal<Value>`.
```
type Signal =
| Constant(Value)
| Closure(... ? ...)
| NamedSignal(SignalName) // ???
```
But... if we allow recompilation at runtime of signals, a constant signal may become something more complex with dependencies.
That's why you always have to track dependencies - even when the original value ain't changing (atleast with the current compiled code)