module exports
According to the module grammar, the following is valid:
691module car { function startCar() {} module engine { function start() {} } export {start:startCar} from engine; }
It seems like there would be issues with exporting module elements after the module has been defined.
I don't see any conflicts with the code you wrote, but it does contain a linking error, because the car module doesn't have access to the unexported start function. Maybe you intended:
module car {
export function startCar() { }
module engine {
export function start() { }
}
export { start: startCar } from engine;
}
In this case, you have a conflict, because the car module is attempting to create two different exports with the same name. This is an early error.
Also, what is the behavior of aliasing over existing Identifiers? Would the compiler fail or would behavior be the 'last' Identifier wins?
Early error.
Yes, thanks, my mistake on the unexported startCar function declaration. My question is more about semantics, if the author of engine did not want to export start, the grammar allows anyone importing the engine module to override the original author's intent.
No, a module determines its exports by itself, and no one can override that. Notice that you missed two export declarations, car.startCar and car.engine.start. If the engine module doesn't export start, then the outer car module cannot access it.
I'm trying to understand the options for exporting things from a module. Here's how I think it works, mostly based on what I see in Traceur. Does any of this look wrong?
To export a value,
export var someName = someValue;
To export a function,
export function someName(args) { ... };
To export multiple things defined elsewhere in this file,
export {name1, name2, ...};
Here's the part that confuses me most. It seems that importers have three options.
- import specific things from a given module
- import everything that was exported from a given module
- import a subset of what a given module exports that it identified as the "default" (presumably the most commonly used things)
To define the default subset of things to export from a module,
export default = some-value or some-function;
where some-value could be an object holding a collection of things to export.
Have you ever used JavaScript module systems before? If so, the idea of a default export should be somewhat familiar…
I have used Node.js extensively. In that environment, as I'm sure you know, a module exports one thing. It can be an object with lots of properties on it, a single function, or a single value. I suppose you could say that all Node has is a "default" export which is the one thing the module exports.
I'm trying to understand how that compares to ES6 modules. I see how in ES6 I can import specific things from a module or I can import everything a module exports. Am I correct that a "default" export can be somewhere in the middle ... a subset of everything that is exported?
I'm trying to understand how that compares to ES6 modules. I see how in ES6 I can import specific things from a module or I can import everything a module exports.
You can't really import all exported bindings. You can import the module instance object itself:
module M from "wherever";
which will give you access to all of the exports.
Am I correct that a "default" export can be somewhere in the middle ... a subset of everything that is exported?
Not really. The default export is literally just an export named "default". There is sugar on the import side, where you can leave off the braces:
import foo from "somewhere";
is equivalent to:
import { default as foo } from "somewhere";
The specialized default export syntax is just plain confusing and should be jettisoned, in my opinion. It would be less confusing for users to simply write:
export { foo as default };
I fail to see why sugar over this form is necessary.
On Fri, Mar 14, 2014 at 8:54 AM, Kevin Smith <zenparsing at gmail.com> wrote:
You can't really import all exported bindings. You can import the module instance object itself:
module M from "wherever";
which will give you access to all of the exports.
That's what I meant by importing all the exports. I'd prefer it if the syntax for that was
import M from "wherever";
That way I could think of import is doing something like destructuring where the other syntax below is just getting some of the exports.
import {foo, bar} from "wherever"';
The specialized default export syntax is just plain confusing and should be jettisoned, in my opinion. It would be less confusing for users to simply write:
export { foo as default };
I fail to see why sugar over this form is necessary.
I completely agree. Plus if this is taken away then the "import" keyword can be used to get the whole module as in my example above. At that point maybe there is no need for the "module" keyword.
I completely agree. Plus if this is taken away then the "import" keyword can be used to get the whole module as in my example above. At that point maybe there is no need for the "module" keyword.
Maybe, but at this point that would be too big of a change to swallow. I think if we can just focus on eliminating this one pointless and confusing aspect (the export default [expr] form), we'll be good to go.
I understand it's hard to make changes after a certain point. It's too bad though that developers will have to remember that the way to import a few things from a module is:
import {foo, bar} from 'somewhere';
but the way to import the whole module is:
module SomeModule from 'somewhere';
instead of
import SomeModule from 'somewhere';
It just seems so clean to say that if you want to import something, you always use the "import" keyword.
Importing is nothing like destructuring. You import mutable bindings; you don't do assignment. I'm very glad that different syntax is used for each case.
What is a 'mutable binding'?
On Fri, Mar 14, 2014 at 10:07 AM, Mark Volkmann <r.mark.volkmann at gmail.com>wrote:
That's what I meant by importing all the exports. I'd prefer it if the syntax for that was
import M from "wherever";
As Kevin said, this already means "import the default export from 'wherever'"
I fail to see why sugar over this form is necessary.
Because it doesn't allow for the Assignment Expression form (specifically, function expressions) that developers expect to be able to write:
export default function() {}
From: John Barton <johnjbarton at google.com>
What is a 'mutable binding'?
// module1.js
export let foo = 5;
export function changeFoo() {
foo = 10;
}
// module2.js
import { foo, changeFoo } from "./module1";
// Import imports mutable bindings
// So calling changeFoo will change the foo in current scope (and original scope)
console.log(foo); // 5
changeFoo();
console.log(foo); // 10
// module3.js
module module1 from "./module1";
let { foo, changeFoo } = module1;
// Destructuring uses assignment to copy over the current values
// So calling changeFoo does not affect this binding for foo
console.log(foo); // 5
changeFoo();
console.log(foo); // 5
Is the common use case for "export default" when you want to give users of the module an easy way to obtain a single function?
So instead of users doing this:
import {someFn} from 'wherever';
they can do this:
import someFn from 'wherever';
Indeed. If you have used Node.js extensively, I am sure you are familiar with this paradigm.
Because it doesn't allow for the Assignment Expression form (specifically, function expressions) that developers expect to be able to write:
export default function() {}
The alternative here is:
function MyThing() {}
export { MyThing as default };
Which is more clear, more readable, and barely less ergonomic. If you really want the AssignmentExpression form, you've got to put the equals in there. I've said this before, but without the equals it looks too much like a declaration:
export default class C {}
var c = new C(); // No C defined, WTF?
Node users don't elide the equals sign, do they?
module.exports = whateva;
So why are we?
Equals aside, let's look at the cost/benefit ratio here:
- Benefit: a little less typing (at most one savings per module)
- Cost: more confusion and StackOverflow questions about default export syntax.
Sigh. The example just better demonstrates how clunky the syntax is and how surprising the semantics can be. :(
<rant>
I hope one of the CommonJS or RequireJS folks write a good ES6 module loader so that I can continue to use reasonable syntax and ignore all of this.
This really smells like Second System Syndrome. The module spec is trying to do too much. Both module objects and mutable bindings. Both defaults and named exports.
There is a certain elegance to the way both RequireJS and CommonJS
reuse fundamental JavaScript patterns (assignment,
functions-with-arguments, objects-with-properties). The gjs
module
system was even smaller, with a single "magic" imports
object. I
really wish the ES6 module system could be chopped down to clearly
express a single idea, and do more with less.
</rant>
On Fri, Mar 14, 2014 at 11:04 AM, Kevin Smith <zenparsing at gmail.com> wrote:
The alternative here is:
function MyThing() {} export { MyThing as default };
Which is more clear, more readable,
I think it's fair to say that these are subjective claims.
and barely less ergonomic. If you really want the AssignmentExpression form, you've got to put the equals in there.
I don't understand this claim, any legal AssignmentExpression form is allowed.
I've said this before, but without the equals it looks too much like a declaration:
export default class C {} var c = new C(); // No C defined, WTF?
Why is this surprising? Named function expressions don't create a lexical binding for their name and therefore cannot be called by that name from outside of the function body:
var f = function a() {};
a(); // nope.
The same thing applies to class expressions, which is what is written in your example--"class C {}" is effectively the same as the expression between "=" and ";" of the following:
var D = class C {};
And no one would expect to be able to this:
var c = new C();
But if you used the export Declaration
form, it will work (as it does
today, without export
of course):
export class C {}
var c = new C();
export function F() {}
var f = new F();
Node users don't elide the equals sign, do they?
module.exports = whateva;
So why are we?
To make a single form that works across platforms (ie. an amd module
doesn't "just work" in node and vice versa). I don't think this is strong
enough to be considered a valid counter-point, I recommend not pursuing it.
export default function() {}
will work the same way on all platforms.
Equals aside, let's look at the cost/benefit ratio here:
- Benefit: a little less typing (at most one savings per module)
- Cost: more confusion and StackOverflow questions about default export syntax.
If a developer knows how named function expression bindings work today, this won't be a big surprise.
On Fri, Mar 14, 2014 at 9:15 AM, Rick Waldron <waldron.rick at gmail.com>wrote:
I think it's fair to say that these are subjective claims.
Indeed, and subjectively I agree with Kevin.
Why is this surprising?
It is surprising because it looks like it should work like
export class C {}
The keyword 'default' looks like a modifier like 'const'.
If a developer knows how named function expression bindings work today, this won't be a big surprise.
I know how named function expressions work and it's still surprising.
var f = function a() {}; a(); // nope.
Sure, note the equals (which is my point).
var D = class C {};
And no one would expect to be able to this:
var c = new C();
Same thing. Note the equals, which gives the reader the necessary visual cue that we are entering an AssignmentExpression context.
But if you used the
export Declaration
form, it will work (as it does today, withoutexport
of course):export class C {} var c = new C(); export function F() {} var f = new F();
Right. The lack of equals sign shows us that this is clearly a declaration.
To make a single form that works across platforms (ie. an amd module doesn't "just work" in node and vice versa). I don't think this is strong enough to be considered a valid counter-point, I recommend not pursuing it.
export default function() {}
will work the same way on all platforms.
Sorry, I don't understand this. ES6 modules, whatever they are, will be the same across platforms.
And if I believe TC39 is making a mistake, I will pursue it : )
I've used es6 modules for several months now and I'm curious to know when I would want to leverage mutable bindings.
I guess I need to begin to imagine that variables bound to imports are really a kind of property name of s secret object:
import { foo, changeFoo } from "./module1";
console.log(foo); // Oh, yeah, this really means module1.foo
changeFoo();
console.log(foo); // Ok, since this is secretly module1.foo, the result '10' makes sense.
I like that more I read about this, more the with
statement comes into my
mind ...
console.log(foo); // Oh, yeah, this really means module1.foo
looks like that
changeFoo();
here the implicit context ? ... if so, I have no idea which one it is and why ... also, can I change it? Maybe I asked for a function utility, not for a trapped context bound into an exported function I cannot change later on
console.log(foo); // Ok, since this is secretly module1.foo, the result '10' makes sense.
nope, not at all, at least here
On Fri, Mar 14, 2014 at 12:24 PM, Kevin Smith <zenparsing at gmail.com> wrote:
Sure, note the equals (which is my point).
...
Same thing. Note the equals, which gives the reader the necessary visual cue that we are entering an AssignmentExpression context.
What about the following:
Functions with return AssignmentExpression that include "=" and without--are the ones without also confusing without the "necessary" visual cue?
function a() {
var F;
return F = function() {};
}
function b() {
var C;
return C = class {};
}
vs.
function c() {
return function F() {};
}
function d() {
return class C {};
}
Or yield in generators?
function * a() {
var F;
yield F = function F() {};
}
function * b() {
var C;
yield C = class C {};
}
vs.
function * c() {
yield function F() {};
}
function * d() {
yield class C {};
}
Sorry, I don't understand this. ES6 modules, whatever they are, will be the same across platforms.
Isn't that exactly what I said? You asked "So why are we?", I answered "To make a single form that works across platforms" and added that amd and cjs don't "just work" together. Then I concluded by with a specific example, but surely that wasn't too misleading?
It is surprising because it looks like it should work like
export class C {}
The keyword 'default' looks like a modifier like 'const'.
I completely agree with this. It looks like a modifier. In addition to not having an = or some other reason to think it will be evaluated as an expression, "default" is a reserved word and has special significance here. Yes, it is grammatically unambiguous and can be learned, but this is a question of intuition. The meaning here goes very strongly against my intuition.
I know how named function expressions work and it's still surprising.
Same here.
If anything, I would say that it makes more sense to go ahead and run with the intuition we seem to be feeling with is that it seems like a modifier of the export. So maybe like:
export default class C {}
var c = new C(); //works
export default function f(){}
f(); //works
export default let obj = {a:1,b:2};
var a = obj.a; //works
On Mar 14, 2014, at 9:27 AM, John Barton <johnjbarton at google.com> wrote:
I've used es6 modules for several months now and I'm curious to know when I would want to leverage mutable bindings.
So cycles work!
// model.js
import View from "view";
export default class Model {
...
}
// view.js
import Model from "model";
export default class View {
...
}
This kind of thing just falls flat on its face in Node and AMD.
I guess I need to begin to imagine that variables bound to imports are really a kind of property name of s secret object:
If that gets you there, that's cool. But it's a bit sloppy. It blurs userland data structures with internal language implementation data structures.
Here's how I think about it. A variable in JS denotes a "binding", which is basically an association with a mutable location in memory. In particular it doesn't denote a value. The binding has a value at any given time.
When you export from a module, you're exporting bindings, rather than values. This means you can refactor between
module m from "foo";
...
m.bar
and
import { bar } from "foo";
...
bar
and they're fully equivalent. But it also means that when you have modules that mutate their exports during initialization, you don't run into as many subtle order-of-initialization issues as you do with AMD and Node, because importing something syntactically early doesn't mean you accidentally snapshot its pre-initialized state.
(Also, keep in mind that the vast majority of module exports are set once and never changed, in which case this semantics only fixes bugs.)
On Mar 14, 2014, at 9:37 AM, Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:
I like that more I read about this, more the
with
statement comes into my mind ...
There's nothing like this in JS today, so if you're only looking for precedent there, you're only going to be able to come up with weak analogies. The differences between aliasing bindings from a module with a fixed, declarative set of bindings, and aliasing bindings from an arbitrary user-specified and dynamically modifiable object are massive.
And see my reply to JJB to get an understanding of why this is such an important semantics. tl;dr non-busted cycles.
Functions with return AssignmentExpression that include "=" and without--are the ones without also confusing without the "necessary" visual cue?
No, because they are preceded by keywords that also indicate an expression context, in this case "return" and "yield".
On Mar 14, 2014, at 10:50 AM, Russell Leggett <russell.leggett at gmail.com> wrote:
I completely agree with this. It looks like a modifier.
This is a good point, and folks working on the Square transpiler noticed this, too. I think there's a more surgical fix, though (and I'm not entertaining major syntax redesign at this point). In fact it's one with precedent in JS. In statements, we allow both expressions and declarations, which is a syntactic overlap. But visually people expect something that looks like a declaration in that context to be a declaration, so the lookahead restriction breaks the tie in favor of declarations.
I think we're seeing the exact same phenomenon with export default
-- the modifiers make it look more like a declaration context. So I think we should consider doing the same thing as we do for ExpressionStatement:
ExportDeclaration :
...
export default FunctionDeclaration ;
export default ClassDeclaration ;
export default [lookahead !in { function, class }] AssignmentExpression ;
This actually results in no net change to the language syntax, but it allows us to change the scoping rules so that function and class declarations scope to the entire module:
// function example
export default function foo() { ... }
...
foo();
// class example
export default class Foo { ... }
...
let x = new Foo(...);
// expression example
export default { x: 1, y: 2, z: 3 };
I argue that not only does this avoid violating surprise, it's not any more of a special-casing logic than we already have with ExpressionStatement, because it's the same phenomenon: a context that allows a formally ambiguous union of two productions, but whose context strongly suggests the declaration interpretation over the expression interpretation in the overlapping cases.
From: es-discuss <es-discuss-bounces at mozilla.org> on behalf of David Herman <dherman at mozilla.com>
So I think we should consider doing the same thing as we do for ExpressionStatement: ... This actually results in no net change to the language syntax, but it allows us to change the scoping rules so that function and class declarations scope to the entire module:
This seems nice; a tentative +1. I like how surgical of a change it is.
Personally I have not found the current state confusing. default
, just like yield
or return
or throw
or others, was a fine signifier for me that we're entering an expression context. But since others don't seem to have adapted, perhaps this will help them. And it is definitely convenient in cases where you want to refer to the default export elsewhere within the module.
ExportDeclaration : ... export default FunctionDeclaration ; export default ClassDeclaration ; export default [lookahead !in { function, class }] AssignmentExpression ;
I think this would allay most of my concerns.
David Herman wrote:
I think we're seeing the exact same phenomenon with
export default
-- the modifiers make it look more like a declaration context. So I think we should consider doing the same thing as we do for ExpressionStatement: <snip>
I agree with the confusion (due in part because the nearly-identical TypeScript syntax puts the exported identifiers in scope), and the fix seems to address the problem so I support it.
On Fri, Mar 14, 2014 at 11:42 AM, David Herman <dherman at mozilla.com> wrote:
When you export from a module, you're exporting bindings, rather than values. This means you can refactor between
module m from "foo"; ... m.bar
and
import { bar } from "foo"; ... bar
and they're fully equivalent.
Ok great, so one solution to potential confusion caused by 'import' is simply to always use 'module'.
But it also means that when you have modules that mutate their exports during initialization, you don't run into as many subtle order-of-initialization issues as you do with AMD and Node, because importing something syntactically early doesn't mean you accidentally snapshot its pre-initialized state.
(Also, keep in mind that the vast majority of module exports are set once and never changed, in which case this semantics only fixes bugs.)
If I am understanding correctly, I'm skeptical because the semantics of these bindings resemble the semantics of global variables. Our experience with global variables is that they make somethings easy at the cost of creating opportunities for bugs. Function arguments and return values provide a degree of isolation between parts of programs. Bindings allow two parts of a program, related only by importing the same module, to interact in ways that two functions called by the same function could not. The coupling is rather like public object properties but without the object prefix to remind you that you are interacting with shared state. That's where the 'module' form may be more suitable: I expect 'm.bar' to have a value which might change within my scope due to operations in other functions.
David I know the analogy was weak but since indeed you said there's nothing like that, I named the one that felt somehow close because of some implicit behavior.
I am personally easy going on modules, I like node.js require and I think that behind an await like approach could work asynchronously too but I don't want to start a conversation already done many times so ... I'll watch from the outside, waiting for a definitive "how it's going to be" spec before even analyzing how that even works.
IMO, modules in ES6 went a bit too far than expected.
Take care
On Fri, Mar 14, 2014 at 3:34 PM, John Barton <johnjbarton at google.com> wrote:
Ok great, so one solution to potential confusion caused by 'import' is simply to always use 'module'.
Another way to put this is that changing:
import { bar } from "foo";
to
module m from "foo";
let bar = m.bar;
will always be a subtle source of bugs.
Looked at another way, the module spec is introducing a new sort of assignment statement, where the bindings are mutable. But instead of adding this as a high-level feature of the language, it's being treated as a weird special case for modules only.
I would be happier introducing a general purpose "mutable binding assignment" like:
let mutable bar = m.bar;
where every reference to bar is always treated as a dereference of
m.bar
. That way the new assignment feature isn't pigeonholed as a
weird part of the module spec.
Couldn't we assemble the desired semantics out of pre-existing
primitives, instead of inventing new stuff? For example, if m.bar
in the example above was a proxy object we could preserve the desired
"mutable binding" without inventing new language features.
--scott
ps. I foresee a future where modules are (ab)used to create mutable bindings. Better to make them first-class language features!
pps. Circular references work just fine in node. You have to be a
little careful about them, but the 'mutable bindings' don't change
that. They just introduce bar
as a new shorthand for writing
m.bar
. IMHO the latter is actually preferable, as it makes it
obvious to the author and reader of the code exactly what is going on.
According to the module grammar, the following is valid:
691module car { function startCar() {} module engine { function start() {} } export {start:startCar} from engine; }
It seems like there would be issues with exporting module elements after the module has been defined. Also, what is the behavior of aliasing over existing Identifiers? Would the compiler fail or would behavior be the 'last' Identifier wins?