DEV Community

Cover image for The Quest for ReactiveScript

The Quest for ReactiveScript

Ryan Carniato on November 23, 2021

This article isn't going to teach you about the latest trends in frontend development. Or look in detail into the way to get the most performance o...
Collapse
 
hackape profile image
hackape • Edited

IMO, the part about svelte's reactive language falling short is only half-truth. In svelte if we want composition we'd start with writable store in the first place, with practically the same reactive language like Vue's.

Giving the $store syntax sugar, it feels very native to svelte's reactive language, and shouldn't be left unmentioned.

let a = writable(0);  // equiv to $ref(0)
$: doubled = $a * 2;

$a = 10;

// or without language sugar magic:

// equiv to watch($$(a), v => {...})
a.subscribe(value => {
  const doubled = value * 2;
  console.log(doubled)
}

a.set(10);

Enter fullscreen mode Exit fullscreen mode

Talking about store being auxiliary, it's actually a good thing, not some kind of burden/cost. Cus it's opt-in, swappable. You can freely switch to redux or rxjs store if you want. You don't get eco locked-in like with Vue's $ref or Solid's createSignal.

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

Right, but as mentioned a couple of times, that is outside of the "language" part. I like Svelte stores. And they solve a very very necessary problem and having sugar makes them feel more native. But the juxtaposition makes it instantly clear of language/compiler limitations. It wraps a 2nd completely different reactive system. If Svelte only used stores I suspect it might not have been so. The insistence on being just JS/HTML by its followers also amplify this.

And really the purpose of this article is wondering if we can somehow find the holy grail. A truly composable reactive system that doesn't introduce a ton of new syntax. Svelte gets most of the way there, but what does all the way look like?

Collapse
 
kennytilton profile image
Kenneth Tilton

Not sure how holy anything I make can be, but my reactive system hides pretty well behind "define_property": tilton.medium.com/simplejx-aweb-un...

I have been enjoying your surveys of reactive alternatives and learned a few new ones! I need to get out more, missed Recoil completely!

Collapse
 
kennytilton profile image
Kenneth Tilton

The problem may be in trying to make standalone vars reactive. But standalone vars may be a requirement because objects got thrown out, because a reactive object can hide reactivity behind reactive accessors. So the real problem, then, is throwing out objects. The funny thing being that objects where different instances can have different reactive definitions for the same property, and where different instances can have additional properties (the prototype model), kinda solves all the problems of OO. But we cannot have objects because ReactJS (how ironic now is that name?) needs functions everywhere so they can control state change, which is also why React add-ons must now let React manage all state. See "concurrent mode". So the real problem may be the impedance mismatch between trying to achieve reactive state in ReactJS, which has officially rejected the paradigm for its eagerness.

the tl;dr for the above is "slippery slope". :)

Collapse
 
ninjin profile image
Jin

Idea about double-destiny operator:

var a = 10;
var b <=> a + 1;

a = 20;
Assert.AreEqual(21, b);

b = 20;
Assert.AreEqual(19, a);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lukechu10 profile image
Luke Chu

This would be impossible to implement in the general case because the compiler would essentially be solving an equation. In most cases, this would be impossible because there could be multiple solutions for a depending on the value of b. In other cases, reversing the operation would simply be impossible, e.g. a hash function.

Collapse
 
azrizhaziq profile image
Azriz Jasni

How about a new keyword?

var a = 10;
reactive b = a + 1; // of course to long :D

a = 20;
Assert.AreEqual(21, b);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ryansolid profile image
Ryan Carniato

Then we are looking at a label/keyword example and that all applies. My point is it is easy to bucket all solutions into these categories or hybrids of them. We can debate the exact naming/syntax but I have been wondering if we can escape this altogether.

Collapse
 
ninjin profile image
Jin • Edited
var a = 10;
ever b = a + 1; // not so long

a = 20;
Assert.AreEqual(21, b);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ryansolid profile image
Ryan Carniato

That's interesting. Scares me a bit. Part of me really wants to make mutation(assignment) special and isolated but I just might not be letting go enough to fully embrace this thinking. Would it ever be difficult to reverse the derivations? Sure subtracting 1 from b is easy enough. I just wonder if that wouldn't always be the case.

Collapse
 
ninjin profile image
Jin • Edited

It's the lens in general. See JS example:

let _a = 10
const a = ( next = 10 )=> return _a = next
const b = ( next )=> a( next === undefined ? undefined : next - 1 ) + 1

a(20);
Assert.AreEqual(21, b());

b(20);
Assert.AreEqual(19, a());
Enter fullscreen mode Exit fullscreen mode

We actively use this that approach in this way:

class App {

    // it's signal
    @ $mol_mem
    static a( next = 10 ) { return next }

    // it's derivation but with same api as signal
    @ $mol_mem
    static b( next ) {
        return this.a( next === undefined ? undefined : next - 1 ) + 1
    }

}

App.a(20);
Assert.AreEqual(21, App.b());

App.b(20);
Assert.AreEqual(19, App.a());

Enter fullscreen mode Exit fullscreen mode
Collapse
 
3shain profile image
3Shain

It only makes sense if the mapping is a bijection (math term). It's a really rare property, meaning zero information loss.

Thread Thread
 
ninjin profile image
Jin

No, It's very common. We can bind local property with part of json which bind with local storage as example. So write to property will change json at local storage and affects to same property of another instance of same app. Example:

class Profile {

    @ $mol_mem
    store() {
        return new $mol_store_local({
            profile: { name: 'Anon' }
        })
    }

    @ $mol_mem
    name( next?: string ) {
        return this.store().sub( 'profile' ).value( 'name', next )
    }

}

const profile = new Profile

profile.name() // 'Anon'
profile.name( 'Jin' ) // 'Jin'
// restart app
profile.name() // 'Jin'
localStorage.getItem( 'profile', '{ "name": "Anon" }' )
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
3shain profile image
3Shain

Bi-directional bindings re-invented? Fair enough.

Collapse
 
yyx990803 profile image
Evan You

FWIW, the Vue example is a bit misleading: you don't need $$() for common reactive effects if using watchEffect:

let value = $ref(0)

// log the value now and whenever it changes
watchEffect(() => console.log(value));

value = 10; // set a new value
Enter fullscreen mode Exit fullscreen mode

$$() is only needed if you have external composition functions that explicitly expect a raw ref object as arguments.

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

Ok. Thanks Evan. I have updated that section to better represent common patterns in Vue. Thank you.

For what it's worth, without completely messing with the semantics I think the Function decoration approach like found in Vue Ref Sugar is the only approach that actually checks all the boxes. But I'm interested in what happens if we do just mess with everything.

Collapse
 
oxharris profile image
Oxford Harrison

These are all interesting experimentations. And here's one approach I've been explororing since last year: Subscript - reactivity without any special syntaxes or language tokens, but just regular, valid JavaScript.

It is implemented as a UI binding language:

<div title="">
  <script type="subscript">
    let title = this.state.title || 'Some initial text';
    this.setAttribute('title', title);
  </script>
</div>
Enter fullscreen mode Exit fullscreen mode

Above, the 'this' keyword is a reference to the <div> element; and this.state.title is the state being observed. Now the let expression evaluates each time the state of this.state.title changes, and the this.setAttribute() call picks up the new value of title each time. This is what happens when state is changed as in below:

let div = document.querySelector('div');
div.state.title = 'New title';
Enter fullscreen mode Exit fullscreen mode

It's that simple; it's pure JavaScript that works reactively by just understanding reference stacks. Details are here: webqit.io/tooling/oohtml/docs/gett...

I moved on implementing it's runtime and making a real world app with it. Waiting to see where this leads.

Collapse
 
ryansolid profile image
Ryan Carniato

I see in this example you have accessor on the state on the element which serves as the signal and the script("subscript") itself is the wrapped effect. Makes sense. I see no problem with runtime reactivity without special syntax. SolidJS works that way today. But the desire for getting rid of the this.___ or state.___ is almost feverish pitch so I thought I'd try my hand at the problem.

Collapse
 
ninjin profile image
Jin

It is interesting to see here the destiny operator, in which we have been using component composition description language for a long time.

A few examples of how we declare object methods (channels):

name \Jin
Enter fullscreen mode Exit fullscreen mode

This is a method that returns a constant string (one way channel).

name?val \Jin
Enter fullscreen mode Exit fullscreen mode

This is the same, but the meaning can be changed (singal in your terminology and two-way channel in ours).

Now the operator of destiny:

greeting /
    \Mr.
    <= name
Enter fullscreen mode Exit fullscreen mode

There is already a derived method that returns an array from a constant string and the values of another method.

And now, the most interesting thing is the bidirectional channel:

title?val <=> name?val
Enter fullscreen mode Exit fullscreen mode

We can read and write in 'title' without even knowing that we are actually working with 'name'.

And then there is the reverse destiny operator:

name => title
Enter fullscreen mode Exit fullscreen mode

It may seem that this is the same as the normal destiny operator, but it matters when linking components to each other:

sub /
    <= Input $mol_string
        value => name
    <= Output $mol_paragraph
        title <= name
Enter fullscreen mode Exit fullscreen mode

Now the Output directly uses the value from the Input, and we control it through the "name" channel.

And yes, Input and Output is channels too which returns cached instance of other components.

Here you can see the generated TS code.

Collapse
 
trusktr profile image
Joe Pea • Edited

I love the concepts. For these new concepts to be adoptable into EcmaScript they'd have to play well with the existing imperative constructs, living along aside them, while reducing any chance for ambiguity.

Maybe the label idea isn't so bad if it propagates into every place it the feature is used, like

import { foo@ } from './foo'

signal count = 0

log(count@)

setInterval(() => foo@ * count@++, 1000)

function log(value@) {
  effect { console.log(value@) }
}
Enter fullscreen mode Exit fullscreen mode

or something.

Now, I'm not sure this is the best syntax, or that it doesn't have any issues, or that @ is the best symbol, but the idea with signal names requiring to be postfixed with @ in the example is

  • usage sites are clear and semantic: we know we're dealing with a signal
  • receiving or passing sites (identifiers in import or export statements, function parameters, etc) have the same naming requirement and can only accept or receive signals (so passing "by ref" or "by value" is not relevant at these sites anymore, we just "receive a signal" or "pass a signal").

Another thing to consider is that, if dependency-tracking were applied to all of JavaScript's existing features, what would this imply for performance?

The performance characteristic of every variable, every property, every parameter, every usage site, would change (probably get slower) just so that reactivity works. With a parallel syntax to keep the imperative and declarative paradigms decoupled, we can limit any overhead strictly to those signal variables and their consumers, without affecting the engine implementation of the other features. This would reduce implementation complexity for people who write and maintain the JS engines.

I'm hoping this will spawn more discussion on the possibilities!

Collapse
 
webreflection profile image
Andrea Giammarchi

I've played around this topic a bit myself, and the gist was something like this:

const invoke = $ => $();
const signal = value => function $() {
  if (arguments.length) {
    value = arguments[0];
    if (effects.has($))
      effects.get($).forEach(invoke);
  }
  return value;
}

const effects = new WeakMap;
const effect = (callback, $$) => {
  const fx = () => callback(...$$.map(invoke));
  for (const $ of $$) {
    if (!effects.has($))
      effects.set($, []);
    effects.get($).push(fx);
  }
  fx();
  return fx;
};
Enter fullscreen mode Exit fullscreen mode

This basically lets one compose values as effects too, example:

const a = signal(1);
const b = signal(2);
const c = effect((a, b) => a + b, [a, b]);

console.log(a(), b(), c()); // 1, 2, 3

a(10);
console.log(a(), b(), c()); // 10, 2, 12

b(7);
console.log(a(), b(), c()); // 10, 7, 17
Enter fullscreen mode Exit fullscreen mode

My thinking is that a variable that effects from others won't ever directly change itself, so that setting c(value) might, instead, throw an error.

As for the syntax, I find the reactive bit being well represented by functions so that let b <- a + 3; doesn't look too bad:

  • it's broken syntax these days, so it can be used/proposed
  • it is the equivalent of () => a + 3; except it accepts zero arguments as it cannot be directly invoked, and the arrow points at the reference that should reflect whatever the body/block returns.
Collapse
 
brucou profile image
brucou • Edited

Interesting stuff. I wrote a language that I never got to implement (of course but who knows if one day I won't find the time to do it) that does what you suggest. The language is simple, and based on the core functional reactive programming concepts. You have events, effects, and pure computations. Excerpts:

-- events. Syntax: event => effect (here state update)
clickPlus => counter <- counter + 1
clickMinus => counter <- counter - 1

-- equations
unit_price = 100
price = counter * unit_price
...

-- effect. Syntax: event => effect
update(price, counter, ...) => render ...
Enter fullscreen mode Exit fullscreen mode

So just three key elements of syntax, event => action notation to express reactions, = for equations that must always hold at any point of time (your const ?), and <- to update an equational variable.

There is nothing particularly smart there. Synchronous reactive languages are not much different than that (Lucid, Esterel, etc.). It is not the smartness, it is more the simplicity of the notation.

For composition, it is simply about noting that a program is a set of events (left side of x => y), variables (left side of x = y), and action/effect handlers (right side of x => y). So Program<Events, Variables, Effects> basically. To get one big Program from two small Programs, just import those two small in the big one and possibly rename the variables, events, effects (to avoid shadowing) - just like you would do with template partials in good old HTML templating systems. But the story was not perfect for composition. Renaming is a hassle that breaks a bit module independence (the big component that reuses the small one need to know about the details of the used component. Ideally you should not need to know about the variables in the reused component, they should be encapsulated).

Haven't put myself to solve these problems though. So it is interesting to read this writing.

Collapse
 
artalar profile image
Artyom

When we talk about reactive language we should think not about reactive programming or even reactive data accessors, but about reactive data structures. The main point of separate language for reactive datas is that it should work perfectly (effecianlty) with a data with all possible (by a language) ways. In other words, the language should expose API for data accesing and transformation and limit it to fit in a most efficient compile output. It means we should be able to analize AOT all accessors in classic for / map and optimize it or throw it away from a language and replace it by some variations of pick: listOfAC = listOfAB.pick({a: 'identity', b: b => toC(b) }).

Collapse
 
ryansolid profile image
Ryan Carniato

I think there probably is some amount of API that is unavoidable but there is something attractive about not having much in the way of API for this. We might have no choice for things like lists. To specially handle compilations for things like array literals. Mostly I don't view this necessarily always been in a runtime primitive mechanism. The reason I focus on language is because as our analysis gets better the implementation could change dramatically. We see this with Svelte and I've seen this taken even further with Marko. Those don't even have reactive primitives anymore at runtime but just call some functions. My hope is that behavior (but not the implementation) can be well defined in mostly with regular language mechanisms. Lists might just have to be the exception.

Collapse
 
henryong92 profile image
Dumb Down Demistifying Dev

Svelte ?:
Solid !: