module exports

# Kam Kasravi (14 years ago)

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?

# David Herman (14 years ago)

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.

# Kam Kasravi (14 years ago)

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.

# David Herman (14 years ago)

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.

# Mark Volkmann (11 years ago)

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.

  1. import specific things from a given module
  2. import everything that was exported from a given module
  3. 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.

# Domenic Denicola (11 years ago)

Have you ever used JavaScript module systems before? If so, the idea of a default export should be somewhat familiar…

# Mark Volkmann (11 years ago)

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?

# Kevin Smith (11 years ago)

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.

# Mark Volkmann (11 years ago)

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.

# Kevin Smith (11 years ago)

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.

# Mark Volkmann (11 years ago)

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.

# Domenic Denicola (11 years ago)

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.

# John Barton (11 years ago)

What is a 'mutable binding'?

# Rick Waldron (11 years ago)

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() {}
# Domenic Denicola (11 years ago)

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
# Mark Volkmann (11 years ago)

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';
# Domenic Denicola (11 years ago)

Indeed. If you have used Node.js extensively, I am sure you are familiar with this paradigm.

# Kevin Smith (11 years ago)

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.
# C. Scott Ananian (11 years ago)

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>

# Rick Waldron (11 years ago)

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.

# John Barton (11 years ago)

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.

# Kevin Smith (11 years ago)
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, without export 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 : )

# John Barton (11 years ago)

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.
# Andrea Giammarchi (11 years ago)

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

# Rick Waldron (11 years ago)

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?

# Russell Leggett (11 years ago)

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
# David Herman (11 years ago)

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.)

# David Herman (11 years ago)

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.

# Kevin Smith (11 years ago)

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".

# David Herman (11 years ago)

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.

# Domenic Denicola (11 years ago)

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.

# Kevin Smith (11 years ago)
ExportDeclaration :
    ...
    export default FunctionDeclaration ;
    export default ClassDeclaration ;
    export default [lookahead !in { function, class }] AssignmentExpression ;

I think this would allay most of my concerns.

# Brian Terlson (11 years ago)

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.

# John Barton (11 years ago)

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.

# Andrea Giammarchi (11 years ago)

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

# C. Scott Ananian (11 years ago)

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.