Draft for a proposal of an Explicit Reference Syntax

Reference to variable is actually implicit in JS and depend on the type. In order to avoid mistake and unexpected behavior I suggest this new syntax. Moreover, it is common to encapsulate a primitive into an object (eg.: array) to reference it. This syntax also aims to prevent global mutable variable declaration used implicitly in functions and give a way to explicitly call them.

Syntax

This syntax introduce 2 new sigil

  • @var (read only reference)

  • &var (read write reference)

Read only references are seen like deep freeze object or const primitives

For variable declaration: sigil is mandtory in reference name from declaration to the end of use

For function: sigil is mandatory in arguments declaration and for arguments call (throws a type error else)

const delared references can't be reassigned

let declared refenrences can be rassigned

Type casts

Ok

  • &var can be cast to @var

  • &var can be cast to var

Throws

  • @var can't be cast to &var

Runtime decision

  • @var can be cast to var but can throws at runtime if trying to mutate

let source

let source2

const &constRef = source

let &letRef = source

&constRef = source2 //error, can't reassign const reference

&letRef source2 //ok

&var can't reference a const declared primitive

A variable can't have multiple mutable reference to prevent memory race


const source = /* any */

const &ref1 = source

const &ref2 = source //Error, source has already a mutable referenced

Examples

Global scope parameter

Old way


let userId = 0

async function getNextUser(url) {

const raw = await fetch(`${url}?id=${userId++}`)

return await raw.json()

}

await getNexUser(url)

With references


let userId = 0

async function getNextUser(url, &userId) {

const raw = await fetch(`${url}?id=${&userId++}`)

return await raw.json()

}

await getNexUser(url, &userId)

Safe reference passing


const array = [1, 2, 3, 4]

computeOnArray(@array) //throws if array is mutate (secure use of the function)

New short notations and syntax help


const array = @[1, 2, 3, 4] //Create an explicit deep freezed array with a simple and clear syntax

const object = @{ key: 'value' } //Same for object

const fn = @function () {} //Or function

Counter


let counter = 0

const counter2 = 0

function increment(&counter) {

&couter++

}

increment(&counter)

increment(&counter2) //Type error, counter2 refers to a const

console.assert(counter === 1)

Call tracker

Track call on function without unknown global mutable variable, specifiyng all context to avoid mistakes.


let counter = 0

function someFunction(args, &counter) {

// stuff

&couter++

}

setInterval(() => someFunction(args, &counter), 200)

Classic arguments declaration


function foo(arg0) {

//

}

let &a

let @b

foo(&a) //Same behaviour as classic declaration for object and reference behaviour for primitives

foo(@b) //throws if foo try to mutate @b, prevent unexpected behaviour

Mandatory reference


function foo(&arg0, @arg1, arg3) {}

let &ref0

let @ref1

let noRef

foo(&ref0, @ref1, noRef) //ok

foo(&ref0, &ref0, noRef) //ok, can cast &ref to @ref

foo(&ref0, @ref1, &ref1) //ok, &ref1 source will be muted

foo(noRef, @ref1, &ref1) //throws, &arg0 need to be an explicit mutable reference

foo(@ref1, &ref0, noRef) //throws, &arg0 need to be an explicit mutable reference

foo(&ref0, @ref1, @ref1) //possibly throws at runtime is arg3 is mutated

Explicit referencing


const a = [1, 2, 3]

const &b = a //explicit reference, no ambiguiti

const @c = a //c refers to a but can only access to deepFreeze(a)

&b.push(1) //no error

@c.push(1) //Type error, object is not extensible

FAQs

  • &var referencing const declared object can be confusing, should it be forbidden ?

  • @ and & sigil choice ?

Proposal: GitHub - JOTSR/proposal_explicit_reference_syntax

Is this allowed?

const mutateCounterFactory = (&x) => () => x++;

{
  let &x = 0;
  mutateCounter = mutateCouonterFactory(&x);
  console.log(x);
  mutateCounter();
  console.log(x);
}

I find the fact that mutateCounter() is able to update the value of &x, without me having to explicitly pass a reference directly into mutateCounter() on that line of code off-putting. This is the sort of mutation mess that can make code hard to follow, because there's almost no guarantees about who might be changing a variable and where. Once you hand it out, it could change at any time without you knowing.

On the other hand, we could model this after C#'s "ref" parameters, where you're only allowed to update the value while the function is running. As a result, In their implementation, lambdas can't assign to reference parameters, and they also don't play nice with async functions, which is unfortunate, and maybe we could discuss other options for JavaScript. But, this kind of model provides some really strong guarantees - you can know that the function is only allowed to mutate the value only for the time it's being called, and as soon as it returns, it can't touch it anymore. Now it's much easier to follow the path of code, and to know who's updating what.

Related thread Passing by reference

Since you use the parent scope to get x, the sigil is mandatory, so this write:

const mutateCounterFactory = (&x) => () => &x++;

Like #private the value cannot be accessed without the sigil.
Async can't cause memory race but passing &ref through Web API like Worker as to be prevent since they have no locks

Got it. Though, that was mostly a slip-up. My main concern lies between these three lines of code (fixed so it has the sigil):

console.log(&x);
mutateCounter();
console.log(&x);

Writing mutable code in programming is often discouraged, because of how hard it is to track where those mutations come from. Of course, it's also sometimes necessary, but adding a lot of new syntax around a discouraged practice doesn't seem ideal to me. For example, looking at those above three lines, there is nothing that's telling you that mutateCounter(); is updating the value &x. It just does it.

So, that's what I'm getting at. I have code that looks like this:

let &x = 0;
const thing = new MyThing(&x);
const anotherThing = thing.getAnotherThing();
const a = thing.calcValue() + anotherThing.calcValue();
anotherThing.doSomeSideEffect();
instanceFromOuterScope.useValue(a);
return &x;

And you look at that. And you wonder... wait, which of those lines of code actually modified &x? Was is the MyThing constructor? Or did MyThing pass &x to anotherThing and anotherThing.doSomeSideEffect() modified it? Or maybe it somehow got passed around to instanceFromOuterScope()? Or maybe all of the above?

Of course, you hope that with some proper naming and a degree of cleanliness that it wouldn't be too hard to actually figure this problem out. But, it certainly helps if there's some guarantees around how a reference can be used. For example, if this was C# and I was passing the variable into the MyThing() constructor using their ref syntax, than I would already have my answer. The only place that's capable of modifying the variable is the MyThing() constructor. The constructor isn't allowed to pass my reference variable around to other places for later use, because once the constructor finishes executing, it's not allowed to update the reference anymore.

I don't known C# ref, I will check it but the previous example is not runnable, here's an example of the implementation of your class

let &x = 0 /* label 0 as source in comments */;
const thing = new MyThing(&x);
/* Can't continue below as class implementation inevitably throws */
const anotherThing = thing.getAnotherThing();
const a = thing.calcValue() + anotherThing.calcValue();
anotherThing.doSomeSideEffect();
instanceFromOuterScope.useValue(a);
return &x;


//Explicit ref declared in constructor
class MyThing {
    constructor(&ref) {
        this.&ref = &ref //throws, source accept only one mutable reference
        this.ref = &ref //throws, source accept only one mutable reference
        this.@ref = &ref //ok

        this.encapse = () => &ref
    }

    getAnotherThing() {
        return new MyThing(this.@ref) //thwrows since @ can't cast to &
        return new MyThing(this.encapse()) //throws, require sigil
        return new MyThing(&this.encapse()) //throws, source accept only one mutable reference (since is equivalent to &(&ref) )
    }
}

//Legacy ref declared in constructor
class MyThing {
    constructor(ref) {
        this.@ref = ref //ok
        this.encapse = () => ref
    }

    getAnotherThing() {
        return new MyThing(this.encapse()) //throws, source accept only one mutable reference
    }
}

//Can't reference thing because of &x */
const anotherThing = thing //throws, thing as a mutable reference of source but source accept only one mutable reference

Oh, ok, interesting.

With this:

this.&ref = &ref //throws, source accept only one mutable reference

I had missed that only one reference to a & variable could exist at a time, but I see now that you had said that in the initial post.

What about this example class - does this have an issue?

class MyThing {
  constructor(&ref) {
    this.encapse = value => { &ref = value }
  }

  getAnotherThing() {
    this.encapse(5);
    return new AnotherThing(this.encapse);
  }
}

class AnotherThing { ... }

Or, is this not allowed either? Is it because there's an assignment happening inside the callback, instead of just getting the reference's value?

Yes, the synthax forbids an assignement of the callback from AnotherThing

class AnotherThing {
    constructor(cb) {
      /* any except @ref forbidden */ = cb
   }
}

It ensure that the syntax prevent any memory race. If you want to get the referenced value to manipulate you need to introduce a specific keyword to copy the intrinsic value (eg.: ```copy &ref````, from Explicit Ownership syntax)

1 Like

Ok, that sounds like it solves my concern :+1: