Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for The Quest for ReactiveScript
This is Learning profile imageRyan Carniato
Ryan Carniato forThis is Learning

Posted on • Edited on

     

The Quest for ReactiveScript

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 out of your website. Instead I want to write about something that I've been playing with in my head for the past year but never can find the time to work on. Reactivity as general purpose language.

If you want someone to blame. Blame Jay Phelps (I kid). After a demo I made showing off the power of fine-grained reactivity he got it in my head that we should look at this more as a generalized language. I was content in my DSL bubble, thinking of ways we can make building frameworks easier, but he challenged me to think about it more generally.

I've been meaning to take him up on his offer, but in the meantime what I can do is write about it. Because the last year I've done a lot of searching and thinking into how I'd approach this. And thanks to more recent conversations around Svelte, Vue Ref Sugar, and my work on Marko etc.. this seems as good time as ever to share what I've learned.

The Destiny Operator

Image description

One of the best introductions I've ever read to reactivity, after the fact isWhat is Reactive Programming?. I can't promise it's the best introduction for the uninitiated. But it introduced reactivity in a very simple way. That reactivity is when an equation which holds true even after its values change. Ifa = b + c, then it is reactive ifa still reflects this sum afterb orc updates.

This article proposes the use the "Destiny Operator"<= to denote this relationship:

vara=10;varb<=a+1;a=20;Assert.AreEqual(21,b);
Enter fullscreen modeExit fullscreen mode

A simple addition to the language but capable of doing so much. Most importantly it highlights the difference between a reactive declaration and an assignment. It makes no sense forb to ever be re-assigned as then its relationship of always being one larger thana wouldn't hold. Whereasa needs to be re-assigned or this system isn't really doing much.

This is just the start. In many ways this has been seen to be the ideal. Reality is a bit more complicated than that. We will return to the "Destiny Operator" a bit later.

Identifiers

If you've ever used a fine-grained reactive library in JavaScript you've seen the common pattern of using function getters/setters. They might be hidden behind proxies but at the core there is an accessor so that values can be tracked and subscriptions made.

const[value,setValue]=createSignal(0);// log the value now and whenever it changescreateEffect(()=>console.log(value()));setValue(10);// set a new value
Enter fullscreen modeExit fullscreen mode

In fact I'd say the majority of frontend JavaScript frameworks have fallen into this 3 part reactive API/language:

  1. Reactive State (Signal, Observable, Ref)
  2. Derived Values (Memo, Computed )
  3. Side Effects (Effect, Watch, Reaction, Autorun)

The example above uses Solid but you should be able to picture that pretty easily in React, Mobx, Vue, Svelte etc. They all look very similar.

For a more in detailed introduction check outA Hands-on Introduction to Fine-Grained Reactivity

The problem is no matter what we do with fine-grained reactivity at runtime there is extra syntax. There is no way at runtime to just havevalue be a value and be reactive. It's going to bevalue() orsomething.value orvalue.something. A small ergonomic detail but one that there is a desire to solve.

The simplest compiler aided approach is decorate the variable identifiers to let it know it should compile to function calls. I first saw this in the frameworkFidan and later in some Babel plugins the community had created forSolid.

letvalue$=createSignal(0);// log the value now and whenever it changescreateEffect(()=>console.log(value$));value$=10;// set a new value
Enter fullscreen modeExit fullscreen mode

What's great about this is no matter the source we can use this syntax sugar:

letvalue$=createCustomReactiveThing();
Enter fullscreen modeExit fullscreen mode

However, now our signal is always treated as a value. How would we pass it outside of this module context and retain reactivity? Maybe we reference it without the$? Do we pass it in a thunk() => value$, do we invent a syntax for this? Do we have control over if the reactive value is readonly? As shown above derived reactive values probably should be. I actually saw a version of this where single$ meant mutable and$$ meant readonly.

The crux though is this syntax doesn't simplify the mental model. You need to be aware exactly what is being passed around and what you are receiving. You are saving typing some characters, possibly as little as 1 as the shortest way to express reactivity without compiler tricks is 2 characters(() or_.v). It's hard for me to consider adding all this is worth it.

Keywords, Decorators, Labels

So how to do this better? Well what if reactivity was a keyword, decorator, or label? MobX has been doing this for ages with decorators on classes butSvelte has taken this to a whole new level.

The basic idea is:

signal:value=0;// log the value now and whenever it changeseffect:console.log(value);value=10;// set a new value
Enter fullscreen modeExit fullscreen mode

Svelte realized that if it treated every variable as a Signal it could reduce that to:

letvalue=0;// log the value now and whenever it changes$:console.log(value);value=10;// set a new value
Enter fullscreen modeExit fullscreen mode

If this draws similarities to the "Destiny Operator" it should. Svelte's$: label is really approaching it. They recognized the "Destiny Operator" was insufficient as you don't only have reactive derivations but side effects like thisconsole.log. In so you can use$: both define variables with reactive declarations like the "Destiny Operator" as well as reactive effectful expressions.

So we're done right. Well no. There are huge limitations of this approach. How does reactivity leave this module? There is no way to get a reference to the reactive signal itself; just its value.

Note: Svelte does have 2 way binding syntax andexport let as a way to do parent to child passing of reactivity. But in general you can't just export or import a function and have it reactive without using an auxiliary reactive system like Svelte Stores.

How do we know what to do with:

importcreateCustomReactiveThingfrom"somewhere-else";letvalue=createCustomReactiveThing();
Enter fullscreen modeExit fullscreen mode

Is it reactive? Can it be assigned? We could introduce a symbol on our identifiers for this case, but we are back to where we were with the last solution. What if you wanted to extract out a derivation likedoubleValue how would the template know what to do with it.

letvalue=0;// can this$:doubleValue=value*2;// becomeconstdoubleValue=doubler(value);
Enter fullscreen modeExit fullscreen mode

Not intuitively. We have a keyword(label) for it and it doesn't transpose.

Function Decoration

Well composition is king. Probably the single most important part ofReact's success and for many of us no composition is a non-starter. Svelte has composition and extensibility through its stores, but the focus here today is in the reactive language where it falls short.

There is another approach that I first came across talking with theMarko team almost 2 years ago. Marko is an interesting language because it heavily values markup syntax, and the maintainers had basically resolved that they wanted to bring their reactivity into their tags.

<let/value =0/><!-- log the value now and whenever it changes --><effect(){console.log(value);}/>value = 10; // set a new value
Enter fullscreen modeExit fullscreen mode

Definitely foreign on the first look but by using tags they'd basically solved Svelte's problem. You knew these were reactive. It is the syntax version of something similar to React's convention thatuse____ is a hook.

Interestingly enough, about a year later Evan You independently came to the same conclusion withversion 2 of his Ref Sugar API forVue 3. Version 1 was labels like above but he realized the shortcomings of that approach and ended up with:

letvalue=$ref(0)// log the value now and whenever it changeswatchEffect(()=>console.log(value));value=10;// set a new value
Enter fullscreen modeExit fullscreen mode

Well it's almost the same thing as the Marko example. This approach actually gives most of what we are looking for. We've regained composition.

However, there is one consideration here still when it comes to passing references out of our current scope. Since Vue is using this as a bit of a syntax sugar like the identifier example earlier it needs to tell the compiler still when it wants to pass by reference instead of by value, and there is the$$() function for that. For instance if we wanted to pass explicit dependencies in:

letvalue=$ref(0)// log the value now and whenever it changeswatch($$(value),v=>console.log(v));
Enter fullscreen modeExit fullscreen mode

Notice howwatch here is just an ordinary function. It couldn't know how to handlevalue any differently. If left alone it would compile towatch(value.value, v => ... ), which would do the reactive access too soon outside a tracking scope.

There are some comments in the proposal asking for a$watch to handle exactly that but I suspect they won't pass because that is specific behavior that$(function) doesn't have. Vue's goal is to be composable, so having$watch be special isn't acceptable. That makes it basically a keyword, as$mywatch wouldn't be known to be given the same behavior, unless we added another syntax or made more general changes to behavior.

In fact none of the solutions, short of Marko's tags, handle that case without extra syntax. Marko can leverage the knowledge of being a tag to make some assumptions you can't make about an ordinary function. And being tags we inadvertently stumbled on what I believe might be the actual solution.

Rethinking Reactive Language

All the approaches suffer from the same challenge. How do we preserve reactivity? We are always worried about losing it, and we are forced into this pass by reference vs pass by value discussion. But that is because we are living in an imperative world, and we are a declarativegirl paradigm.

Let me elaborate a bit. Marko uses a<const> tag for declaring reactive derivations. Our "Destiny Operator" so to speak. This sometimes confuses people because derived values can change so how is it "const"? Well it never gets re-assigned and the expressions holds for all time.

When I was trying to explain this to someone new, Michael Rawlings(also on the Marko team) clarified it was thelet(Signal) that was special not theconst(Derivation). Every expression in our templates act like a derivation, every attribute binding, component prop. Our<const value=(x * 2)> is no different than a<div title=(name + description)>.

Which got me thinking what if we've been looking at this all backwards. What if expressions were reactive by default and instead we needed to denote the imperative escape hatches? Instead of a "Destiny Operator" we'd need a side-effect operator.

This seems crazy because would it be intuitive to change the semantic meaning of JavaScript yet keep the same syntax? I assumed no, but I mean we've already seen this done to great success. Svelte's scripts are nothing like "plain JavaScript" yet people seem to be accepting of those and some even advertising them as such.

I did poll a while back and while not conclusive the results suggested many developers are much more sensitive to syntax than semantics.

Image description

So the question is can we do something using the existing syntax of JavaScript and keep all the tooling advantages(even TypeScript)? I mean completely mess with how it executes in the way things like Svelte, React Hooks, or Solid's JSX defies expectations but do so with pure JavaScript syntax and in way people can make sense of. Well, we can try.

Designing ReactiveScript

For all of my, what might sound like criticism, over decisions made in the various approaches above there is a lot of great prior work to tap into. I think Svelte today is a good starting point as it has simple syntax and already distorts the expected semantics. Taking the example from above picture we want to hoist theconsole.log into another function (maybe imported from another module). This isn't something Svelte does today but maybe something like this:

functionlog(arg){$:console.log(arg);}letvalue=0;// log the value now and whenever it changeslog(value);value=10;// set a new value
Enter fullscreen modeExit fullscreen mode

For the sake of visualizing how things actually behave I'm going to "compile" these down to Solid's explicit runtime syntax. Although this being runtime based isn't a requirement.

functionlog(arg){createEffect(()=>console.log(arg());}const[value,setValue]=createSignal(0);// log the value now and whenever it changeslog(value);// or log(() => value())setValue(10);// set a new value
Enter fullscreen modeExit fullscreen mode

All function arguments get wrapped in functions (or pass the function straight through). All local scoped variables get called as functions.

How about if we want to create a derived value? In our new reactive world that might look like:

letvalue=0;constdoubleValue=value*2;// log double the value now and whenever it value changeslog(doubleValue);value=10;// set a new value
Enter fullscreen modeExit fullscreen mode

Or we could even hoist it out:

functiondoubler(v){returnv*2;}letvalue=0;constdoubleValue=doubler(value);
Enter fullscreen modeExit fullscreen mode

Which could compile to:

functiondoubler(v){return()=>v()*2;}const[value,setValue]=createSignal(0);constdoubleValue=doubler(value);
Enter fullscreen modeExit fullscreen mode

You might be scratching your head at this example because well does anything ever run? Well it doesn't unless it needs to. As in it is used in a side effect denoted by$:. We have a lazy evaluated language that only runs code when absolutely needed.

Our derived value is still assigned to aconst so it remains consistent. No need for new syntax to know exactly what its behavior is. In a sense reactive values don't escape their local scope like in Svelte from a mutation standpoint but they do from a tracking standpoint. The retains clear control while affording the convenience of local mutation.

This "every expression is reactive" can extend to language primitives as well. In a similar way to how Solid transforms ternaries in JSX we could look at things likeif andfor statements and compile them accordingly.

letvalue=0;if(value<5){log("Small number");}elselog("Large number");// logs "Small number"value=10;// logs "Large number"
Enter fullscreen modeExit fullscreen mode

This code would end up running both branches of theif once the condition changes. And those side effects don't need toconsole.logs at all and could be anything like maybe JSX.

What if you could write components like this and have it work with minimal executing fine-grained reactivity.

functionComponent({visible}){letfirstName,lastName="";if(!visible)return<p>Hidden</p>;// only do this calculation when visibleconstfullName=`${firstName}${lastName}`return<><inputonInput={e=>firstName=e.target.value}/><inputonInput={e=>firstName=e.target.value}/><p>{fullName}</p></>}
Enter fullscreen modeExit fullscreen mode

Just a taste

Honestly, there is a ton of details to work through. Like loops for example. We naturally want a.map operator rather than afor in this paradigm so how do we reconcile that? However what this has going for it is, it is analyzable and the pattern applied consistent.

Performance of such a system might require a lot more consideration. I think this actually has more potential with additional analysis and compile time approaches. Looking at whatlet/const are actually stateful could inform what to wrap or not. And once on that path, well, this goes many places. It could be used as a tool for things like partial hydration to know exactly what code actually can update and be sent to the browser.

Honestly this is just an idea for now. And I have a lot more thoughts on how this could function. But with all the recent discussions I thought someone might be interested in exploring this and I encourage them to reach out and discuss!

Top comments(25)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
hackape profile image
hackape
  • Joined
• Edited on• 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 modeExit 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'screateSignal.

CollapseExpand
 
ryansolid profile image
Ryan Carniato
Frontend performance enthusiast and Fine-Grained Reactivity super fan. Author of the SolidJS UI library and MarkoJS Core Team Member.
  • Location
    San Jose, California
  • Education
    Computer Engineering B.A.Sc, University of British Columbia
  • Work
    Principal Engineer, Open Source, Netlify
  • Joined
• Edited on• 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?

CollapseExpand
 
kennytilton profile image
Kenneth Tilton
Started on the Apple II and Integer Basic, went pro on PDP-11 and COBOL, found Common Lisp way to late.
  • Location
    Jersey Shore, USA
  • Education
    Clark University
  • Work
    Publish my own software and O/S fine-grained reactive libs for JS, CLJS and Lisp.
  • Joined

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!

CollapseExpand
 
kennytilton profile image
Kenneth Tilton
Started on the Apple II and Integer Basic, went pro on PDP-11 and COBOL, found Common Lisp way to late.
  • Location
    Jersey Shore, USA
  • Education
    Clark University
  • Work
    Publish my own software and O/S fine-grained reactive libs for JS, CLJS and Lisp.
  • Joined

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". :)

CollapseExpand
 
ninjin profile image
Jin
Homepage: jin.hyoo.ruDonations: boosty.to/hyoo
  • Location
    SPb
  • Work
    Founder at HyOO
  • Joined

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 modeExit fullscreen mode
CollapseExpand
 
azrizhaziq profile image
Azriz Jasni
🧑‍💻 ❤️ 🐈
  • Location
    Malaysia
  • Joined

How about a new keyword?

vara=10;reactiveb=a+1;// of course to long :Da=20;Assert.AreEqual(21,b);
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
ryansolid profile image
Ryan Carniato
Frontend performance enthusiast and Fine-Grained Reactivity super fan. Author of the SolidJS UI library and MarkoJS Core Team Member.
  • Location
    San Jose, California
  • Education
    Computer Engineering B.A.Sc, University of British Columbia
  • Work
    Principal Engineer, Open Source, Netlify
  • Joined

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.

CollapseExpand
 
ninjin profile image
Jin
Homepage: jin.hyoo.ruDonations: boosty.to/hyoo
  • Location
    SPb
  • Work
    Founder at HyOO
  • Joined
• Edited on• Edited
var a = 10;ever b = a + 1; // not so longa = 20;Assert.AreEqual(21, b);
Enter fullscreen modeExit fullscreen mode
CollapseExpand
 
lukechu10 profile image
Luke Chu
  • Joined

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.

CollapseExpand
 
ryansolid profile image
Ryan Carniato
Frontend performance enthusiast and Fine-Grained Reactivity super fan. Author of the SolidJS UI library and MarkoJS Core Team Member.
  • Location
    San Jose, California
  • Education
    Computer Engineering B.A.Sc, University of British Columbia
  • Work
    Principal Engineer, Open Source, Netlify
  • Joined

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.

CollapseExpand
 
ninjin profile image
Jin
Homepage: jin.hyoo.ruDonations: boosty.to/hyoo
  • Location
    SPb
  • Work
    Founder at HyOO
  • Joined
• Edited on• Edited

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

let _a = 10const a = ( next = 10 )=> return _a = nextconst b = ( next )=> a( next === undefined ? undefined : next - 1 ) + 1a(20);Assert.AreEqual(21, b());b(20);Assert.AreEqual(19, a());
Enter fullscreen modeExit 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 modeExit fullscreen mode
CollapseExpand
 
3shain profile image
3Shain
  • Location
    University of Edinburgh
  • Joined

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
Homepage: jin.hyoo.ruDonations: boosty.to/hyoo
  • Location
    SPb
  • Work
    Founder at HyOO
  • Joined

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 Profileprofile.name() // 'Anon'profile.name( 'Jin' ) // 'Jin'// restart appprofile.name() // 'Jin'localStorage.getItem( 'profile', '{ "name": "Anon" }' )
Enter fullscreen modeExit fullscreen mode
Thread Thread
 
3shain profile image
3Shain
  • Location
    University of Edinburgh
  • Joined

Bi-directional bindings re-invented? Fair enough.

CollapseExpand
 
oxharris profile image
Oxford Harrison
Full-time web tooling dude making cool concept cars for the web. I work on frameworks, compilers, CLIs and browser APIs. Currently building the world's most advanced Reactive Programming runtime!
  • Work
    Open Source Lead at WebQit, Inc. USA
  • Joined

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:

<divtitle=""><scripttype="subscript">lettitle=this.state.title||'Some initial text';this.setAttribute('title',title);</script></div>
Enter fullscreen modeExit fullscreen mode

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

letdiv=document.querySelector('div');div.state.title='New title';
Enter fullscreen modeExit 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.

CollapseExpand
 
ryansolid profile image
Ryan Carniato
Frontend performance enthusiast and Fine-Grained Reactivity super fan. Author of the SolidJS UI library and MarkoJS Core Team Member.
  • Location
    San Jose, California
  • Education
    Computer Engineering B.A.Sc, University of British Columbia
  • Work
    Principal Engineer, Open Source, Netlify
  • Joined

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 thethis.___ orstate.___ is almost feverish pitch so I thought I'd try my hand at the problem.

CollapseExpand
 
yyx990803 profile image
Evan You
Independent open source developer. Creator / project lead of Vue.js, Vite, and connoisseur of sushi.
  • Joined

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

letvalue=$ref(0)// log the value now and whenever it changeswatchEffect(()=>console.log(value));value=10;// set a new value
Enter fullscreen modeExit fullscreen mode

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

CollapseExpand
 
ryansolid profile image
Ryan Carniato
Frontend performance enthusiast and Fine-Grained Reactivity super fan. Author of the SolidJS UI library and MarkoJS Core Team Member.
  • Location
    San Jose, California
  • Education
    Computer Engineering B.A.Sc, University of British Columbia
  • Work
    Principal Engineer, Open Source, Netlify
  • Joined
• Edited on• 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.

CollapseExpand
 
webreflection profile image
Andrea Giammarchi
  • Joined

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

constinvoke=$=>$();constsignal=value=>function$(){if(arguments.length){value=arguments[0];if(effects.has($))effects.get($).forEach(invoke);}returnvalue;}consteffects=newWeakMap;consteffect=(callback,$$)=>{constfx=()=>callback(...$$.map(invoke));for(const$of$$){if(!effects.has($))effects.set($,[]);effects.get($).push(fx);}fx();returnfx;};
Enter fullscreen modeExit fullscreen mode

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

consta=signal(1);constb=signal(2);constc=effect((a,b)=>a+b,[a,b]);console.log(a(),b(),c());// 1, 2, 3a(10);console.log(a(),b(),c());// 10, 2, 12b(7);console.log(a(),b(),c());// 10, 7, 17
Enter fullscreen modeExit fullscreen mode

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

As for the syntax, I find the reactive bit being well represented by functions so thatlet 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.
CollapseExpand
 
ninjin profile image
Jin
Homepage: jin.hyoo.ruDonations: boosty.to/hyoo
  • Location
    SPb
  • Work
    Founder at HyOO
  • Joined

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

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

name \Jin
Enter fullscreen modeExit fullscreen mode

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

name?val \Jin
Enter fullscreen modeExit 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 modeExit 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 modeExit 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 modeExit 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 modeExit fullscreen mode

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

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

Here you can see the generated TS code.

CollapseExpand
 
trusktr profile image
Joe Pea
  • Joined
• Edited on• 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'signalcount=0log(count@)setInterval(()=>foo@*count@++,1000)functionlog(value@){effect{console.log(value@)}}
Enter fullscreen modeExit 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!

CollapseExpand
 
brucou profile image
brucou
  • Joined
• Edited on• 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 + 1clickMinus => counter <- counter - 1-- equationsunit_price = 100price = counter * unit_price...-- effect. Syntax: event => effectupdate(price, counter, ...) => render ...
Enter fullscreen modeExit 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 (yourconst ?), 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). SoProgram<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.

CollapseExpand
 
artalar profile image
Artyom
Frontend tech lead
  • Joined

When we talk about reactive language we should think not about reactive programming or even reactive data accessors, but about reactive datastructures. 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 classicfor /map and optimize it or throw it away from a language and replace it by some variations ofpick:listOfAC = listOfAB.pick({a: 'identity', b: b => toC(b) }).

CollapseExpand
 
ryansolid profile image
Ryan Carniato
Frontend performance enthusiast and Fine-Grained Reactivity super fan. Author of the SolidJS UI library and MarkoJS Core Team Member.
  • Location
    San Jose, California
  • Education
    Computer Engineering B.A.Sc, University of British Columbia
  • Work
    Principal Engineer, Open Source, Netlify
  • Joined

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.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Free, open and honest software education.

Read our welcome letter which is an open invitation for you to join.

More fromThis is Learning

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp