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.
Can you maybe make a quick example of something fancier like how your proposal would work with jQuery 1.8 style modules if you have multiple things depending on the AJAX module for example. It sounds like you have to know a lot about the other modules to make it work in your proposal.