Labelled Scoping

Hi,

I stumble upon this quite often, but I've never really thought of raising a proposal.

Let's say we need to initialize a variable in scope2 and want it accessible in scope1. You need to write code like this...

scope1: {
  let text;
  
  scope2: {
    text = "Hello";
  }
}

This has some problems...

  • Needs extra typing for IntelliSense and error handling in Typescript.
  • Not possible to declare a constant.

My idea is to prefix the variable declaration with the name of the label given to your scope in which you want to use that variable. Here is a use case and example w/ callbacks...

Instead of this...

function () {
  let data;

  fs.readFile("path.txt", "utf8", (ex, content) => {
    data = content;
  });
  
  // ...
}

We can do...

scope1: function () {
  fs.readFile("path.txt", "utf8", (ex, content) => {
    scope1 const data = content; // It scopes data to scope1 rather than the fs callback.
  });
  
  // ...
}

Need to make a variable global? Hmm... use the predefined global label!

Svelte example...

onMount(async () => {
  global let state = await fetch("localhost:5000/api/state");
})

Let me know what you think :grinning:. Thanks!

1 Like

Asynchronously declaring a variable, or possibly never declaring it, seems pretty confusing all to avoid a let in the desired scope and an assignment inside the inner scope.

1 Like

I've got to say, it sounds like a great idea, but it can already be confusing enough to try and figure out where a variable is defined. Is it a local variable? Many it was defined at the module level? Perhaps it got imported? Adding another location , "within any nested scope", probably won't help make code more readable.

3 Likes

Hi @ljharb,

I realized that in the fs example, data will give undefined because the reading is asynchronous. I'll change that, a mistake on my part :)

Secondly, I meant label scoping to be just syntactic sugar over my first example.

scope1 let data = content;

Above only means that data should be "declared in scope1", but "initialized in this scope".

So this...

scope1: {
  scope2: {
    scope1 let text = "Hello";
  }
}

Is equal to this...

scope1: {
  let text;

  scope2: {
    text = "Hello";
  }
}

That also means you can do crazy stuff like this...

scope1: {
  console.log (text); // Gives undefined but does not throw error.

  scope2: {
    scope1 let text = "Hello";
  }
}

That's because I want Javascript to recognize this statement and declare data at the very top of the specified scope, here scope1.

Then it should initialize it in the current scope.

It should also be able to somehow reinitialize a constant.

Thanks :)

Thanks @theScottyJam!

You're right. It's better to just declare the variable in the wanted scope and initialize it elsewhere.

But this could be useful for the short list of problems I mentioned :)

It will throw, just like this:

console.log(x); // throws ReferenceError
let x; // initializes x = undefined

You cannot access a variable declared with let or const before initialization.

That's strange. I just ran it with node.js and it gave me undefined.

@lightmare - he's saying a labeled declaration is syntax surgar for moving the declaration up to the top of the scope that's been labeled, so that example should in fact give undefined as he said.

But then the type-inference argument is moot, because the variable can have undefined value.
And you also need a different rule for const, because it has to throw ReferenceError before initialization, you can't just move it to the top initialized to undefined and reassign later.

1 Like

It would, but as soon as you enable this option, you've added another place for people to look for declarations, even if the code creator never uses this feature.

For example:

import ...lots of stuff... from somewhere
import ...more stuff... from somewhere
...more imports...

class LargeClass {
  doX() { ...lots of logic... }
  doY() { ...lots of logic... }
  ...more functions...
}

export z

Tell me, where is this "z" variable defined in the code snippet above. In Javascript today, you know its definition does not come from the class, so it's got to be imported. Either that, or someone has grossly made z into a global variable (which you can verify if you don't find "z" in the imports).

Given your new feature, you may have to go through the entire class definition as well to see if it's been defined there.

Editor tooling can help with finding variable definitions, but it's nice when the language provides appropriate restrictions, so you can easily read and follow code in any kind of editor.

1 Like

@lightmare, Not sure if I follow you, I meant this...

scope1: {
  let text; // Text is "any" type

  scope2: {
    text = "Hello"; // Does not change text type
  }
}
scope1: {
  scope2: {
    scope1 let text = "Hello"; // Text is "string" type
  }
}

I guess, we'll need to do let text: string for Typescript or /** @type {string} */ in Jsdoc for text to be string. It can get larger for more complex types :)

Yup, we can't declare a constant without initializing it, but I doubt there won't be a way to do that :)

scope1: {
  console.log(text); // ???
  scope2: {
    scope1 let text = "Hello"; // Text is "string" type
  }
}

On the line marked with ???, either text === undefined, in which case its type is not string, or it throws ReferenceError — like I said previously it should, to match current behaviour of let/const that initialize the binding at the point of declaration, not immediately after entering their scope.

You're trying to come up with var declaration on steroids, that includes more typing and more gotchas. Like a variable declared multiple times, or never actually:

outer: {
  inner: for (let i = 0; i < 5; ++i) {
    const r = Math.random();
    if (r < 0.3) {
      outer const x = 1;
      outer let y = 5;
    } else if (r < 0.6) {
      outer const x = "bar"; // what if it's already === 1
      outer let y; // assign undefined, or leave untouched?
    }
  }
  console.log(x);
}