Destiny Operator

Take an example...

let a = 0;
let b = a + 1;

a = 2;
b = a + 1;

Here throughout the program b is supposed to be the successor of a. Each time we reassign a, we need to explicitly mention b = a + 1.

Can we have a destiny operator as mentioned in this article that will bind a variable to its dependencies?

let age = 13;
const isEligible <= age > 18; // -> False

In the above above code, isElgible is bound to age > 18. Instead of storing a boolean, it keeps an expression in memory. Whenever we modify age and log isEligible, it should call the stored expression and return the result.

age = 20;
isEligible // -> True

Svelte uses a dollar label for this. It'd be nice to have something similar in JS. This will make variables reactive without needing to explicitly update them.

Thank you :grinning:.

In JS, “expressions” are stored in functions (including methods and accessors).

Here is an object-oriented solution to your issue:

let someone = { age: 13, get isEligible() { return this.age > 18 } };
someone.age = 20;
someone.isEligible; // true

or, in case you have more than one person:

const personPrototype = { get isEligible() { return this.age > 18 } };
let someone = { age: 20, __proto__: personPrototype };
2 Likes

This relates to something aclaymore mentioned a while back:

I can’t find a link but I have a vague memory of someone saying that this would go against a key invariant of JS - that accessed or writing to a variable in strict mode will never run arbitrary code.

Assuming accurate, while allowed for accessor properties as seen with claudepache's example, the original example would violate this given that isEligible is a variable and not a property.

1 Like

I also don't find function-call syntax to be that verbose.

Instead of this:

let age = 13;
const isEligible <= age > 18; // -> False
age = 20;
console.log(isEligible) // -> True

we can just do this

let age = 13;
const isEligible = () => age > 18;
age = 20;
console.log(isEligible()) // -> True

It's exactly the same, except you put a () every time you want to access the value.

1 Like

There's a deferred part of decorators proposal that could help you with this: variable decorators.
You'd wrap the expression in a function, and the decorator would make every read call the function:

let @getter isEligible = () => age > 18;
2 Likes

That deferred part won’t fly for the same reasons previously explained in the thread.

2 Likes

My bad, I got the wording wrong. What I meant to say is... well take an example:

const todos = [
  { title: "Water the plants", done: false },
  { title: "Buy milk", done: true }
];

const todosToDo = () => todos.filter((todo) => !todo.done);

Here every time we call todosToDo for something like logging, it recalculates all the todos that are not done. This impacts the performance. Instead, we can bind todosToDo to todos so that it is only updated when todos is changed.

const todosToDo <= todos.filter((todo) => !todo.done);

It doesn't have to be <=. It could be anything even a keyword.

We could take this to function level...

<= console.log(`Todos changed ${todosToDo}`);

Above, console.log is called whenever todosToDo is changed. This could be powerful for debugging.

Hope I have explained it enough. Thank you :sweat_smile:.

This piece of code here

const todosToDo <= todos.filter((todo) => !todo.done);

gets really tricky to implement in an engine. You're asking Javascript to watch a whole lot of unique values for changes, including:

  • The todos array itself (are new items added or removed?)
  • Array.prototype.filter (What if someone replaced the array's filter function on the prototype? We would need to re-evaluate this expression)
  • The done property of each todo in the array

In other words, this feature will create a lot of overhead, which could easily outweigh any potential optimizations.


I'll explain the "proper" way to do this kind of feature in a functional language, just so we can have another perspective on this problem.

  • Firstly, since I'm giving an example from othe functional paradigm, there won't be any variable mutations (as that's a side-effect, which is not a functional thing to do).
  • Next, we'll place our expression into a function, and explicitly pass any needed parameters to it
  • Finally, if we want to optimize things a bit, we would memoize the function, so that it remembers previous results. There's ways to fine toon a memoization algorithm to better fit the performance needs of the particular scenario.

Memoization isn't the only way to solve the type of issue you're describing, but it is one possible solution. In Javascript, it requires you to always keep your data structures immutable, so that you can safetly compare by identity (otherwise, you would have to use an expensive comparison algorithm to check if data has changed).

2 Likes

The ‘reactivity’ part of VueJS can be used independently and adds dependency tracking and memoization.

array example in tests

2 Likes

Seems to me like you're looking for a shortcut to do expensive processing.

function ToDo(desc) {
   let done = false;
   let listener = null;
   return {
      title: desc,
      set listener(fn) {
         listener = fn;
      },
      get done() { return done; }, 
      set done(val) {
         val = !!val;
         if (val && val != done) {
            done = val;
            if (typeof(listener) == "function") {
               listener(this);
            }
         }
      }
   }
}

function TodoList(...args) {
   let list = [];
   let done = [];
   let retval = {
      add(arg) {
         if (arg.done) {
            done.push(arg);
         }
         else {
            arg.listener = listener;
            list.push(arg);
         }
      },
      get todos() { return list.slice(); },
      get done() { return done.slice(); },
      get all() { return this.todos.concat(this.done); }
   };

   function listener(obj) {
      list.filter(item => obj != item);
      if (!done.includes(obj)) {
         done.push(obj);
      }
   }

   for (let arg of args) {
      retval.add(arg);
   }

   return retval;
}

This is just what it looks like to do your example somewhat efficiently. Trying to make something like this both generically and efficiently is just not suitable for a language like ES.

1 Like

Hi @theScottyJam,

Thank you for your response!

const todosToDo <= todos.filter((todo) => !todo.done);

I'm new to functional programming, so if I'm right when todosToDo is referenced, it checks if any of its dependencies have changed (Here the todos array). If not, it takes the value from the cache. If yes it recalculates the expression.

I think there is a library called reselect that does this.

I've come to think of it and guess a dependant keyword would be better.

let a = 0;
dependant b = a + 3;

What I proposed is a bit different. In the above code, b is dependant on a. So when a is modified, if it has dependants, it updates them.

dependant console.log(`Hello ${a}`);

Above console.log is dependant on a. So whenever a is modified it gets called.

I'm not sure if this is the right way to do this, but thank you :grinning:.

Is there a reason you want this to be a core part of the language, when libraries can already offer this type of functionality?

1 Like

The whole expression is also dependant on globalThis["console"], globalThis["console"]["log"] and a["toString"]. Any of those can change.

2 Likes

Maybe better to have opt-in to reactivity. F.e. reactive foo = 123, and console and console.log are not reactive. With bike sheddable naming:

reactive a = 124
auto console.log(`Hello ${a}`);

It would log only when a changes. Everything else is a regular var as had always been, therefore not tracked.

1 Like

If you like that idea, I'd recommend you draw some noise to this proposal of mine.