diff --git a/src/lang/SIGNAL.md b/src/lang/SIGNAL.md index abf4ee4..bdeb0d5 100644 --- a/src/lang/SIGNAL.md +++ b/src/lang/SIGNAL.md @@ -20,7 +20,7 @@ let-signal { // SIGNAL WORLD (RHS of :=) // We extract values from the graph. x := signal-expr-1, - y := signal-expr-2 + y := signal-expr-2, . // NORMAL WORLD (Body) // We compute a new value using the extracted data. @@ -40,9 +40,14 @@ We also have a constant signal expression `const e` where `e` is a normal expres Note that a normal expression can't contain `@` nor `let-expr`. +Broadly we divide signals into sources and closure-signals. +- Sources are either constants or externally directed signals. They don't have any dependencies on other signals. +- Closure-Signals are signals that depend on other signals (either other closure-signals or sources). + For example let's assume `count` is a signal producing some integers, then `let-signal { x := @count . *($x, 2) }` is the `double` signal. + # Top-Level Constructs -We introduce two new top-level constructs, via keywords `signal` and `fn-signal`. +We introduce three new top-level constructs, via keywords `signal`, `cell`, and `fn-signal`. The following ``` @@ -61,6 +66,34 @@ signal sigName { } ``` +We also have a way to introduce new source signal called a cell +``` +cell (counter-dispatch, counter) 0 { count, msg . match $state { +| #inc . +($count, 1) +| #dec . -($count, 1) +| #reset . 0 +| #set-to x . $x +}} +``` +The above defined a signal `@counter` initialized with value `0`, and defines a `counter-dispatch` "effect". +Via the `counter-dispatch` we can send messages (values) to the counter signal, which is gonna be updated via +the update-function (that's given `count` and `msg`). + +Cells are signals together with a way to set them. You can think of them as "actors" holding state and being updated based on a message (which is just a regular value). + +TODO: What is an effect exactly? How can we perform it? What are their types? +TODO: Can you consider cells that depend on other signals somehow? Or maybe that doesn't really make sense? Hmm. The only way to change it should be via the dispatch. + +General syntax +``` +cell (dispatch-name, signal-name) normal-expr-init-state { state, msg . normal-body-expr } +``` +Also consider wordier syntax: +``` +cell (dispatch-name, signal-name) starts: normal-expr-init-state receives: { state, msg . normal-body-expr } +``` + + We also have parametrised top-level signal expressions. These can be used to define UI components ``` fn-signal Counter(color) { @@ -102,434 +135,58 @@ signal App { TODO: Now that I'm thinking about it, I think we should separate Views from Components. Basically a `View` is a pure function that happens to return UI. While a Component is a View together with all the signals feds into it. +# Cells +It is really unclear to me, what the `dispatch` is. Do I need to introduce a new syntactic category for something like "commands"? + +Also the basic static html elements can be clearly represented. +For components that change the view based on some state: I can use a signal that produces html. +But something like a `button` seems mysterious to me. To make a button, I need to specify the `on-click` thing. +But what the hell is that? I want to keep things as pure as possible and delay side-effects as far as possible. But this seems fundamentaly impure. Is this the correct point at which to do the side-effects? + # Runtime Constraints & Implementation -- Static Topology? We should be able to know the dependency graph between signals at compile-time. -- Rank-based Update, topological-srot on graph once at startup. Each node gets a rank (how close it is to the root signal). - Updates ar eprocessed via priority-queue - low-rank dependencies are processed earlier +At the start we load all the top-level functions, signal definitions etc. +Then we take all the signal expressions and evaluate them. +This creates a DAG (Directed Acyclic Graph) of nodes that have inputs and outputs. We track rank-info so +that we can efficiently traverse parts of the graph in a topological-sort order. This allows us to eliminate glitches. + +Then the fun part: We allow - while the system is running - hot-swapping of signals. +We may have a signal that's defined by e.g. a `let-signal` expression. We may change the signal-expr and attempt a hot-reload. This creates a checkpoint, in which the new expression is evaluated - this can spawn new nameless signals - and then we check if we can safely disconnect current signal from old parents (and do cleanup) and if we are cycle-free - if there's failure, we return to the checkpoint while cleaning up the new stuff. If we suceed, we disconnect the signal from old parents, connect new parents, and we propagate the new value. + +Since signals can carry any values - including closures - by default while propagating we're not doing a check about whether anything changed, because e.g. closures don't support structural equality. But in our language we allow to specify for specific signals whether when recomputed, should they propagate to children or not... these are called relational-barriers (in a relational barrier we get access to the previous value and the new value and we compare them and then decide whether to change and propagate to children). + +Signals can also be subscribed-to by external observers - that's how side-effects happen. When a new value is being propagated, it first propagates to all the descendants without notifying the external observers. Only once the graph has succesfuly propagated the new value, the external observers are notified. + +Also, in some sense the DAG is "static" - it can't change its topology when signals do their own thing. The only way +to change it is if user does the hot-swap or spawns new signals - and that's not programatic - user has to do this manually. So in some sense our DAG is compile-time known - even though we don't relaly have the concept of compile-time. + +# External Observers + +The UI renderer is the primary external-observer. +It can subscribe to a signal that returns html, and actually do some stuff, +like initially create and mount the DOM elements. But when the signal changes, it needs to mutate what it has created. + +How to do this well? The only changes are from signals... in the end, we probably have some signal-closure +that evaluates to an html value while observing other signals. When one of the dependencies changes, +how is the re-evaluation done? + +There's some sort of incremental-computation thing going on here. +We have an initial value being computed, and then stuff changes. But only one tiny part may change in the output as a result of the change in input. How to do this efficiently and not recompute everything from scratch? + +We can ofcourse do diffing... But that sucks. - -# TODO: Linear Types - -What exactly is the meaning of the below? -Seems a bit weird that the signal `state` can be accessed multiple times like this. -``` -let-signal { - x0 := @state, - x1 := @state, -. ... -} -It would make sense to me if we sampled once, then duplicated the value we got... -let-signal { - x := @state, - x0 := $x, - x1 := $x, -. ... -} -``` -But then again, I guess it does make sense to sample twice - but there's no guarantee of getting the same result... e.g. -``` -@ : Signal(A) -> Signal(A), A -``` -the signal read operation may potentially change the signal itself. - - - ------------------------ - -This is Solidjs. How does it work? +# Side-Effects +How to do some side-effects? Imagine that you have a counter cell, and when that reaches 5, some side-effect to the +world should happen. ``` -function Counter() { - const [ count, setCount ] = createSignal(0); - return ( - - ); -} - -let-signal foo = x0; -@foo // read -@foo := x1 // write - -@foo { x . body } - - -function Counter() { - let-signal count = 0; - - -} - - - -let count = 0 -let double = count * 2 - -count = 11 -console.log(double) // 0 - - - -// ===Count=== -let-signal count = 0; - -//asignment -@count := 1 -@count := 2 - -// subscription -@count { x . - console.log(x); -} - -// POSSIBILITY 1: manual... -let-signal double = @count -@count { x . - @double := 2*x -} - -// POSSIBILITY 2: automatic -let-signal??? double = 2*@count -// but where' sthe dependency on count? Is it just auto? -// I guess you can just analyze the code and see that this deopends on the count... -// but what if I wanted to just initialize `double` with the current value of the @count signal... -// -// maybe that's not how signals should be used... -// if a signal is used in a thing - that thing must change according to the signal - no other way? -``` - - -What can we do? -- Creation: We can introduce a new signal -- Reading/Subsribing: We can read the current value of a signal - - should we then automatically change with that signal? - Are we dependent on it? - This is like we can read only by subscription. -- Updating: If we have the capability, we can update the signal to new value (can be a function of the current value) -- ???Subscription: We can subscribe to changes in the signal -- Map: We can create new signals that are a function of the original signal: - But this is a bit weird... in this output signal we can still set it... - Maybe we should be able to create only read-only signals? - - -Read/Write Signal -ReadOnly Signal (from map?) - -What about stuff like `andThen`? - -``` -Signal(A) -``` - - - - - -``` -type Model -create initState: Model -type Msg -create update : Msg, Model -> Model - -const { dispatchMsg: (Msg) -> Void } = useElm(initState, update) -``` - -how to call the combination of a signal together with the events? - - - -=== Tracking Scopes === - -When are tracking-scopes created? -``` -// effects -createEffect(() => { - ... -}) - -// returning jsx from a component -// would be better if we had to actually wrap it explicitely into something. -return ( -