ES Modules: suggestions for improvement

David Herman dherman at mozilla.com
Fri Jun 29 16:33:11 PDT 2012


On Jun 27, 2012, at 1:06 PM, Isaac Schlueter wrote:

> On Wed, Jun 27, 2012 at 11:51 AM, David Herman <dherman at mozilla.com> wrote:
>> That bug was particularly bad because it was *assigning* to an accidentally global variable. But in my personal experience I certainly forget to import common libraries like 'path' and 'fs' in Node all the time and end up with unbound variable references. When this happens in a control flow that got missed by tests, then it can end up in production.
> 
> You mean something like this?
> 
> var fs = require('fs')
> // no path here...
> function notCoveredByTests () {
>  fs.open(path.resolve("yabbadabba"), ...)
> }

Right.

> How would any of this solve that?

Because `path` is unbound, and static variable checking reports that as an error.

>>> var Foo = require("foo")
>>> var f = new Foo()
>> 
>> Just import it directly:
>> 
>> import Foo from "foo";
>> var f = new Foo();
> 
> But wait... those are two different things, aren't they?  Isn't yours
> more akin to: `var Foo = require('foo").Foo`?

Yes. I was thinking there wasn't any significant difference in convenience, but was forgetting (until SubStack pointed it out to me on IRC) that the main difference is that the library is required to give the abstraction a name, and the client must import it by that name. The client can rename it explicitly if they like, but that's strictly less convenient than having the export be completely anonymous.

James Burke has also urged us to consider allowing modules to export a single distinguished thing. I'm going to mull this more. I agree it's a worthwhile goal. But I'd like to find a way to keep the syntax as lightweight as possible and yet not interfere with static resolution.

> I'm having trouble articulating why it is that module.exports=blah is
> better than exports.blah=blah.

I think I can make at least a partial case for why it's a good style. In many languages, I think a natural pattern emerges where a module provides a central organizing data abstraction, and there's a special distinguished export representing that data abstraction. Obviously this is popular in NPM, but you see it all over. In Java, they didn't even *have* a module system because classes did double-duty as a data abstraction, a constructor, a type definition and a module. In ML, people use the pattern where a module exports a distinguished type that conventionally is called `t` -- so you would name the module after the type, and the type would be MyWidget.t.

But we should not force this style on programmers. Even Node itself does not adhere strictly to that style -- look at the 'path' or 'fs' libraries, for example. Same with the ES standard library: Math and JSON are both multi-export (pseudo-)modules that just export functions. In these cases, there's no natural data abstraction needed, no class or object with methods, just functions. To quote John Carmack, "Sometimes the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function."

>> Moreover, it would be hostile to adding static constructs in the future, such as macros, that can be exported from a module.
> 
> Can you elaborate on that?

It took me a few days, but I wrote up some rationale for static module resolution on my blog:

    http://calculist.org/blog/2012/06/29/static-module-resolution/

> That's sort of like unfinished objects, then, but with the keys all
> set to undefined.
> 
> So, then `export x = 10` hoists the `export x` and leaves the `x = 10`
> where it is, var-like?

Correct.

> Does a_c === c, or not?

The syntax wasn't quite right. You had:

> // c.js
> import a from "a"
> export c_a = a
> export c = 10
> // does c_a === c?

You could write:

// c.js
import a from "a"
export var c_a = a
export var c = 10

In this case, it acts like traditional hoisting as you say. So the answer depends on the order of execution of the modules. With cycles, there's always a bit of arbitrariness about it; we'll of course make it deterministic, but you can't avoid the possibility of one module referring to another one before it's executed. So if c_a will only be === to a if a.js executes first. And NaN notwithstanding, of course. :)

Alternatively, you could write:

// c.js
import a from "a"
export { c_a: a }

In this case, you're re-exporting the same binding from "a", and they are aliases. No matter what you do, c_a and a will be the same.

Dave



More information about the es-discuss mailing list