`import` and aliasing bindings
Your expectations must be different than mine. :)
Dave and Sam may have a different answer, but I'd answer that the aliasing semantics follows from a module system's role as (among other things) a name spacing mechanism. That implies two axioms:
-
You should always be able to access an export under its qualified or unqualified name, with the same meaning. That is,
M.a = 5;
and
import a from M; a = 5;
should have the same meaning.
-
You should always be able to wrap existing code into a module without changing its meaning. That is, given
let a = 4; // ... a = 5;
it should be possible to refactor 'a' (plus other declarations) into a module without changing the meaning of use sites, like:
module M { export let a = 4; // ... } // ... import a from M; a = 5;
I think that the loss of either of these properties would make modules far more surprising, and refactoring code into modules harder and more error-prone.
I wouldn't worry too much about current source-to-source compilers for ES6 not getting this right yet. That is mainly due to the lack of a detailed specification. I could enumerate quite a few other fundamental things that they don't have right yet (e.g. recursive linking, static checking, etc.).
However, I agree that the destructuring syntax for module imports may not be the clearest choice in terms of raising the right expectations. (As we have discovered already, it has other issues, namely people confusing the direction of renaming.) Maybe it is a bit too clever, and considering alternatives a worthwhile idea.
module "foo" {
export let state = 5;
export function modifyState() {
state = 10;
};
}
import { state, modifyState } from "foo";
assert(state === 5);
modifyState();
assert(state === 10);
That's it.
This is, to me as an ES5 programmer, very weird. There is no other situation in the language where what an identifier refers to can change without me assigning to it.
Chicken and egg. It's doesn't exit because ES doesn't have modules. Node "modules" are just functions, remember.
This is compounded by the syntax used by
import
. The pseudo-destructuring microsyntax, plus the superficially declaration-like nature of that line, make you expect it to behave like a normallet
orvar
declaration with destructuring assignment, which we are used to from CoffeeScript, Python, JS 1.8, etc. Obviously, it's a very different animal.
Yes - and I agree that false symmetry should be avoided. On the other hand, it would be awkward to have the import renaming syntax completely out-of-line with destructuring syntax.
Finally, I can't shake the feeling I'm missing something. Why is this aliasing property valuable, given that it's so contradictory to expectations? I am totally on board with the need for static analysis and thus top-level-only import and export statements. The "just copy Node" peanut gallery is wrong, and the static analysis is not only necessary for asynchronous browser loading, but also adds a lot of power to the language. But what does the aliasing add? Is it connected to static analysis in some way I don't understand?
Well, it also allows proper circular dependencies, for one.
On Dec 28, 2012, at 11:19, "Andreas Rossberg" <rossberg at google.com<mailto:rossberg at google.com>> wrote:
On 28 December 2012 16:20, Domenic Denicola <domenic at domenicdenicola.com<mailto:domenic at domenicdenicola.com>> wrote:
Finally, I can't shake the feeling I'm missing something. Why is this aliasing property valuable, given that it's so contradictory to expectations?
Your expectations must be different than mine. :)
Dave and Sam may have a different answer, but I'd answer that the aliasing semantics follows from a module system's role as (among other things) a name spacing mechanism.
This idea of modules as namespaces is a very interesting one I hadn't considered. My experience is with C++/C# namespaces, and with ES5 modules. With only this experience, the two concepts seem worlds apart. Are there other precedents where something by the name of "modules" acts like C++/C# namespaces, e.g. Ruby or Python? If not, perhaps the feature should be renamed to avoid confusion?
I guess this point of view explains why there was so much TC39 interest in import *
.
I think that the loss of either of these properties would make modules far more surprising, and refactoring code into modules harder and more error-prone.
To who, though? I'm hesitant to draw on existing ES5 module systems as precedent, given their major flaws with regard to static compilation (and the underlying priorities these flaws reveal). But if you're talking about refactoring existing code, and surprisingness to existing ES programmers, I think you have to consider module usage today.
You could argue that "most" ES programmers aren't using modules today, so preserving the intuitions of and making refactoring easier for that minority isn't valuable. You might be correct, but I think the most avid early adopters who will drive ES6 forward are precisely the ones using ES5 modules. Furthermore, many of those not using modules are using "namespaces" via global object hierarchies, which (without with
) have no aliasing properties.
On Dec 28, 2012, at 8:32 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
Dave and Sam may have a different answer, but I'd answer that the aliasing semantics follows from a module system's role as (among other things) a name spacing mechanism.
This idea of modules as namespaces is a very interesting one I hadn't considered. My experience is with C++/C# namespaces, and with ES5 modules. With only this experience, the two concepts seem worlds apart. Are there other precedents where something by the name of "modules" acts like C++/C# namespaces, e.g. Ruby or Python?
There are indeed. Scheme and ML are examples. (Erlang and Haskell aren't really relevant since their bindings are immutable.)
If not, perhaps the feature should be renamed to avoid confusion?
The word "namespace" flies around with many meanings -- you won't get very far arguing about its true meaning. But in terms of language features, ES6 modules are far closer to the historical precedent of language features known as "modules" rather than language features known as "namespaces."
You could argue that "most" ES programmers aren't using modules today, so preserving the intuitions of and making refactoring easier for that minority isn't valuable. You might be correct, but I think the most avid early adopters who will drive ES6 forward are precisely the ones using ES5 modules. Furthermore, many of those not using modules are using "namespaces" via global object hierarchies, which (without
with
) have no aliasing properties.
The aliasing isn't observable for immutable bindings, only for mutable ones. In my experience, all of the module exports I've seen from NPM modules I've used were immutable. Yehuda mentioned that he has seen mutable ones, but are they even common? To be clear, I'm not talking about an exported function that can produce different results when called multiple times (m.currentFoo()) -- that is not affected by the aliasing semantics -- and I'm not talking about an exported object whose contents are mutable (m.stuff.foo) -- that is also not affected by the aliasing semantics. I'm only talking about an export whose binding itself is mutated.
So I fully agree with all of Andreas's points. Another one is that I've been thinking we should add getter/setter exports to make it possible to create lazily initialized exports:
let cell;
export get foo() {
if (!cell)
cell = initialize();
return cell.value;
}
It would be surprising for a getter property to extract its value at import time rather than reference time, and even more surprising for a setter property not to invoke the setter when mutated via an import. It would defeat the purpose of import. A big part of the reason for import to be a different form from let/var is for it to set up this aliasing relationship. If all it did was local binding, it would not be offering much that let/var don't already do.
So I fully agree with all of Andreas's points. Another one is that I've been thinking we should add getter/setter exports to make it possible to create lazily initialized exports:
let cell; export get foo() { if (!cell) cell = initialize(); return cell.value; }
Don't like it. This is tacking on features to a part of the language that should be nearly featureless. There are plenty of abstraction mechanism to accomodate lazy initialization already (one could export a class which has a static getter, for example).
On 28 December 2012 17:54, David Herman <dherman at mozilla.com> wrote:
Another one is that I've been thinking we should add getter/setter exports to make it possible to create lazily initialized exports:
We haven't had the opportunity to discuss that one, but now that you mention it, I should say that I actually think exports as accessors are a no-go. Because with that, an innocent plain variable occurrence can suddenly be an expression with arbitrary side effects, resurrecting one of the worst features of 'with'. Please don't! If you need lazy initialization, export a function.
On Dec 28, 2012, at 11:54, "David Herman" <dherman at mozilla.com> wrote:
On Dec 28, 2012, at 8:32 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
Dave and Sam may have a different answer, but I'd answer that the aliasing semantics follows from a module system's role as (among other things) a name spacing mechanism.
This idea of modules as namespaces is a very interesting one I hadn't considered. My experience is with C++/C# namespaces, and with ES5 modules. With only this experience, the two concepts seem worlds apart. Are there other precedents where something by the name of "modules" acts like C++/C# namespaces, e.g. Ruby or Python?
There are indeed. Scheme and ML are examples. (Erlang and Haskell aren't really relevant since their bindings are immutable.)
If not, perhaps the feature should be renamed to avoid confusion?
The word "namespace" flies around with many meanings -- you won't get very far arguing about its true meaning. But in terms of language features, ES6 modules are far closer to the historical precedent of language features known as "modules" rather than language features known as "namespaces."
Ok, I feel a bit better. It might be worth debating which "history" the messaging should be based on, i.e. more academic or functional languages vs. ones ES5 programmers are familiar with. But this is not that important; it's just messaging.
You could argue that "most" ES programmers aren't using modules today, so preserving the intuitions of and making refactoring easier for that minority isn't valuable. You might be correct, but I think the most avid early adopters who will drive ES6 forward are precisely the ones using ES5 modules. Furthermore, many of those not using modules are using "namespaces" via global object hierarchies, which (without
with
) have no aliasing properties.The aliasing isn't observable for immutable bindings, only for mutable ones. In my experience, all of the module exports I've seen from NPM modules I've used were immutable.
This is amusingly tied to the single-export question. I.e. module.exports =
produces a single immutable export (with mutable properties, see e.g. npm.im/glob), but all exports.foo =
exports are mutable. Nobody does Object.defineProperty(exports, "foo"', ...)
really. So most exports in npm are immutable… but maybe not for the reason you'd expect.
On Dec 28, 2012, at 9:12 AM, Andreas Rossberg <rossberg at google.com> wrote:
We haven't had the opportunity to discuss that one, but now that you mention it, I should say that I actually think exports as accessors are a no-go.
It's just a thought, and I can see already that it's controversial, so I'll hold off on that issue for another day.
On Dec 28, 2012, at 9:16 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
The aliasing isn't observable for immutable bindings, only for mutable ones. In my experience, all of the module exports I've seen from NPM modules I've used were immutable.
This is amusingly tied to the single-export question. I.e.
module.exports =
produces a single immutable export (with mutable properties, see e.g. npm.im/glob), but allexports.foo =
exports are mutable. Nobody doesObject.defineProperty(exports, "foo"', ...)
really. So most exports in npm are immutable… but maybe not for the reason you'd expect.
Doesn't matter what the reasons are, though; the point is that this isn't going to break existing patterns. Whereas if you don't alias, then you will cause code to break when it does expect it. Another example where this could come up is initialization. Since imports tend to be at the beginning of a module, you could end up reading the value of a not-yet-initialized variable too soon.
On Dec 28, 2012, at 12:28, "David Herman" <dherman at mozilla.com> wrote:
Another example where this could come up is initialization. Since imports tend to be at the beginning of a module, you could end up reading the value of a not-yet-initialized variable too soon.
A code example of this would be awesome. Also, is it a problem ES5 module systems fail to address?
On Dec 28, 2012, at 9:39 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
On Dec 28, 2012, at 12:28, "David Herman" <dherman at mozilla.com> wrote:
Another example where this could come up is initialization. Since imports tend to be at the beginning of a module, you could end up reading the value of a not-yet-initialized variable too soon.
A code example of this would be awesome. Also, is it a problem ES5 module systems fail to address?
It can come up with mutual recursion between modules, which ES5 module systems famously handle poorly. Here's an example, untested, obviously :)
// a.js
import { B } from "b";
export class A {
m() { return new B }
}
// b.js
import { A } from "a";
export class B {
m() { return new A }
}
+1 for adding getter/setter exports to make it possible to create lazily initialized exports. This could be very useful for frameworks to keep footprint lower without causing unintuitive designs.
Benoit
why poorly supported? that logic is easy to reproduce in ES5 modules, isn't it?
// a.js
var B;
function A() {}
// runtime each time
A.prototype.m = function () {
return B ? new B : (B = require('B'), new B);
};
// lazily optimized
A.prototype.m = function () {
B = require('b');
return (A.prototype.m = function () {
return new B;
})();
};
exports.A = A;
I think cross dependency should rarely be an advantage since I find it most of the time an architecture problem ... not sure is a good thing that's simplified, I like the lazy require possibility in any case for all other things such require what you need when you need and not always regardless. This is good practice in node.js modules too.
+1 for adding getter/setter exports to make it possible to create lazily initialized exports. This could be very useful for frameworks to keep footprint lower without causing unintuitive designs.
No - we need to keep "features" out of the module system. Abstractions should be created with the abstraction mechanisms of the language: functions, classes, etc.
However, I agree that the destructuring syntax for module imports may not be the clearest choice in terms of raising the right expectations. (As we have discovered already, it has other issues, namely people confusing the direction of renaming.) Maybe it is a bit too clever, and considering alternatives a worthwhile idea.
There's this:
import x as y, a as b from "url";
But I find that much less readable than:
import { x: y, a: b } from "url";
Destructuring syntax is what it is. Developers just need to start reading it the right way.
On Dec 28, 2012, at 10:31 AM, Kevin Smith <khs4473 at gmail.com> wrote:
However, I agree that the destructuring syntax for module imports may not be the clearest choice in terms of raising the right expectations. (As we have discovered already, it has other issues, namely people confusing the direction of renaming.) Maybe it is a bit too clever, and considering alternatives a worthwhile idea.
There's this:
import x as y, a as b from "url";
But I find that much less readable than:
import { x: y, a: b } from "url";
Right. It also gets more verbose very quickly.
Destructuring syntax is what it is. Developers just need to start reading it the right way.
Agreed.
Now that I have fully digested Andreas's points from the earlier thread on modules 1, I am a bit concerned about the implications of
import
introducing aliasing bindings. To recap, the situation is:module "foo" { export let state = 5; export function modifyState() { state = 10; }; } import { state, modifyState } from "foo"; assert(state === 5); modifyState(); assert(state === 10);
This is, to me as an ES5 programmer, very weird. There is no other situation in the language where what an identifier refers to can change without me assigning to it. Properties of objects, sure. But not bare identifiers. (Well, I guess
with
blurs this line. And the global object. No comment.)This is compounded by the syntax used by
import
. The pseudo-destructuring microsyntax, plus the superficially declaration-like nature of that line, make you expect it to behave like a normallet
orvar
declaration with destructuring assignment, which we are used to from CoffeeScript, Python, JS 1.8, etc. Obviously, it's a very different animal.Notably, such aliasing is out of line with all current ES6 transpilers (Traceur, Six, Yehuda's js_module_transpiler; TypeScript doesn't allow importing from modules, just importing module instance objects). Emulating such semantics would necessitate a desugaring that imports the module instance object, then rewrites all references to the imported identifiers to instead refer to properties of the imported module instance object. This is much more of a transformation than you'd expect, and of course falls down in the face of e.g.
eval("sta" + "te")
. The very fact that such a rewriting to object-property-access is necessary underscores how strange imported identifiers really are. I think people are in for a rude surprise moving from the transpiled world to the native ES6 world, and such semantics mean that a hybrid future---e.g. transpile for IE<=10, native for IE>=11---is laden with footguns waiting to be set off.Finally, I can't shake the feeling I'm missing something. Why is this aliasing property valuable, given that it's so contradictory to expectations? I am totally on board with the need for static analysis and thus top-level-only import and export statements. The "just copy Node" peanut gallery is wrong, and the static analysis is not only necessary for asynchronous browser loading, but also adds a lot of power to the language. But what does the aliasing add? Is it connected to static analysis in some way I don't understand?
P.S. One solution, if aliasing is indeed super-valuable, would be to move away from pseudo-destructuring microsyntax to one that's more alien, and thus connotes that something "strange" (or at least "unlike destructuring") is going to happen. Perhaps
import <state, modifyState> from "foo"
. This could be symmetrized on the export side:export
takes a weirdExportSpecifierSet
that looks something like an object literal, but behaves nothing like it, so maybeexport <state, modifyState>
would make this weirdness clearer.