EDIT There is a second version of this proposal down in the comments. https://gist.github.com/445adc206d51a038de4a#gistcomment-359858
I really like Isaac's module proposal. Having recently designed a module system for the luvit project (node ported to lua), I've thought a bit about this.
I think we can set a few minimal rules and reduce a ton of edge cases. One in particular is cyclic dependencies. I don't think they are worth the pain and should simply be disallowed.
To help understand things, we should make a distinction between dependencies and peers. In a large application or package there are several files containing code. Some files depend on things defined in other files before they can start. Simple dependencies that block startup can't be cyclic or you'll have a deadlock at startup. So the other type of dependency is wanting to reference an api or function from another file during runtime.
The basic syntax I propose is like the one Isaac showed (if I understand it correctly). There are two new keywords, import
and export
. The import
keyword must be followed by a string literal that is the identifier for the file you want to import. The export
keyword is like return
and yield
in that it sends a value. It does not affect control-flow, but it does set the current file's export value.
Consider lib/add.js
:
// lib/add.js
function add(a, b) {
return a + b;
}
export add;
And then we have a peer file that depends on this:
// lib/usesAdd.js
var add = import "./add.js";
var result = add(2, 3);
Now suppose we wanted to write a math package that contains add as well as other functions.
// math.js
export {
add: import "./lib/add.js",
multiply: import "./lib/multiply.js",
// ...
};
Here is where this gets tricky. Suppose that we want to implement multiplication using addition and a loop. So our multiplication library will need to depend on add. I can't depend on the main app because that depends on it. So it has to depend on add directly.
// lib/multiply.js
var add = import "./add.js";
function multiply(a, b);
var product = 0;
for (var i = 0; i < b; i++) {
product = add(product, a);
}
return product;
}
But not we learn later on that add needs to access multiply for some reason. This happens all the time in real applications. How would this be required?
I really want dependencies with import and export to be expressible as a DAG (Directed Acyclic Graph). There is a clear order for loading the executing the files. There are two reasons for this. One, it makes a lot of nasty edge cases with current module systems simply go away. And second, JavaScript is single threaded. Even with the exports.foo = bar
syntax in node and CommonJS, the dependencies have to be executed in some serial order. This may surprise people when you're allowed to require some module, but it's methods aren't populated yet because you were executed before it was. This would be a leaky abstraction.
So then how would I solve this dilemma using acyclic dependencies? I would handle it at the application level where I control the logic and know what to expect.
I would rewrite math.js
as follows:
// math.js
var math = {};
export math;
math.add = (import "./lib/add.js")(math);
math.multiply = (import "./lib/multiply.js")(math);
Then in each module I would export a function that accepts the math
namespace.
// lib/add.js
export function (math) {
return function (a, b) {
// ... might use math.multiply
};
};
There are several other techniques that come to mind that I could employ for other use cases. The point is that using the basic primitives that would be provided by the language, I can organize my app however I want. My code would use the same style in the browser or in node. The browser could pull my code on demand as it's needed or a server-side compiler could statically analyze the imports and generate a single js file that the browser then reads. All JS code everywhere would be written using the same simple module syntax, but without imposing huge constraints on the runtime or providing leaky abstractions to applications.
I have several independent comments.
A: I like the direction. It is fair to say that with @izs’s proposed single-exported-value, you can still have cyclic dependencies and you still need to use unfinished exports objects, but the complexity of supporting cycles is moved out of the module system and into the modules. This is well aligned with the philosophy of making simple things easy while leaving complex things possible.
B: My second proposal to TC39, after a year of CommonJS in the wild, introduced the same
(import "foo.js")
statically-analyzable dependency syntax. @dougcrockford’s comment at the time was that it should beimport("foo.js")
, presumably sinceeval
sets a precedent in JavaScript for operators masquerading as function calls. I concur.C: With this particular solution,
export
could bereturn
.D: For what it’s worth,
import
could also just berequire
. That is, when JavaScript is normally evaluated,require("foo.js")
would be a function call, but when compiled by the system module loader would be statically analyzable syntax.C + D: With these two alterations, we get a graceful migration path, where we can write scripts that target both old and new evaluation contexts.
However, with or without @creationix’s amendments or my C and D amendments, I gather that @izs’s proposal still utterly eliminates the possibility of importing new syntax (macros), the door @dherman’s proposal is intended to leave open, although the proposal does not walk through said door. This leaves us in a bit of a pickle; most of us are not in a position to evaluate whether macros are worth the price and too easily dismiss their value.
It might be helpful for @dherman to provide a strawman for JavaScript macros in the context of his Harmony modules proposal so that we can see why this simplification would not work, and so we might evaluate whether it would be worth the extra complexity. I gather that naming individual exports is the important feature for macros.
E: Module blocks are an independent issue. My opinion is that they provide very little value. @dherman likes to draw small circles around bits of code in files. I prefer small files. It’s a value-call. Given that "module" is proposed to be a contextual keyword that would not interfere with my "module" variable names, I could happily ignore their existence, but I would be even happier knowing they don’t exist.
F: Bulk import * is an independent issue, a value-judgement too. My objection would mostly go away if we were limited to one "import *" at the top of a module, much like we are limited to single, prototypical inheritance. That way I would have a good clue of where to look next for my locally unbound variable, perhaps following a chain of "import *" calls, but never exploding into a search of the dependency tree with a resolution order to contend with (depth-first? breadth-first? C3 monotonic?). That said, I can and do live better without "import *" at all. An equivalent to "import *" was in my original proposal to CommonJS (include("foo.js"))—they talked me out of it—we’ve lived happily without it.
G: Losing the ability to early-detect unbound free variables is an independent issue.