Modules feedback, proposal

James Burke jrburke at gmail.com
Fri Mar 30 15:25:17 PDT 2012


First I'll give some smaller bits of feedback, and then at the end
propose something that integrates all the feedback.

I hope they can considered independently: if you do not like the
proposal, feel free to just take in the feedback. I tried to order the
feedback in order of "craziness" with the first item being least
crazy.

========
Feedback
========

Some of these just may be drift between current thinking and what is
on the wiki. Sorry for anything that is already covered by current
thinking, I mostly just have the wiki as a reference:

-----------
1) Multiple mrls for System.load():
--------------
Dave Herman's latest post on "Synchronous module loading in ES6":
http://calculist.org/blog/2012/03/29/synchronous-module-loading-in-es6/

mentions only allowing System.load() in inline HTML script tags. I'm
assuming that is similar to the "Loader.prototype.load( mrl, callback,
errback )" API in:
http://wiki.ecmascript.org/doku.php?id=harmony:module_loaders

Since System.load() is the only allowed option in HTML script tags, it
should allow specifying more than one mrl in the call. Either:

System.load("jquery.js", "underscore.js", function ($, _) {})
or
System.load(["jquery.js", "underscore.js"], function ($, _) {})

-----------
2) Default module path resolution
-----------
In the "module path resolution" thread, Dave mentioned that the
module_loaders resolve API should allow for some custom path
resolution logic. However it would be good to specify the default
logic.

For the default logic, it would be great to see it be:

baseUrl + ID + ".js"

unless the ID contains a protocol, or starts with a "/", then ID is
assumed to be an URL and used as-is. I'm sure the real default
behavior needs a stronger definition, and I would like to see the
equivalent of AMD loaders's "paths" config as an option for default
config, but the main point is that in code, the IDs for
importing/referencing  would look like this:

import $ from "jquery";

instead of this:

import $ from "jquery.js";

I am just using an arbitrary import example, not sure what the most
current import syntax is. Nothing else implied here for "import", but
the ability to use "jquery" for the name and it works by default given
the rules above.

This is a similar module ID-to-path resolution used by AMD loaders
today, and it has worked out well for us because it allows setting up
references to "implementation providers" vs. particular URLs. This is
nice when using a few libraries that depend on the same piece of
functionality. A concrete example:

I use jQuery in my app, and I use Backbone, which also uses jQuery.
Ideally my script that directly uses jQuery and the Backbone module
can both do:

import $ from "jquery";

and it gets resolved to the same URL. With the "paths" config that is
possible in AMD loaders, we can map that "jquery" to be a CDN version,
or a local version, or a local version that is actually Zepto, just
something that can stand in for jQuery. I like the idea of having a
simpler "paths" config for that instead of requiring the developer to
implement a resolver function or worse include a library to set up the
resolver (might as well just use an AMD loader then :).

-----------
3) Modules IDs as strings
-----------
This item of feedback is assuming that the way to get better optimized
code delivery is to "concatenate" module files together. However, even
if that is not the case for browser delivery, I still believe that
allowing a way to combine a set of modules together in a file helps
just with code sharing -- it is easier to handle and pass around a
single JS file for distribution, but the library dev may still want to
work with the modules separately on disk.

With that assumption, module IDs should always be strings. This makes
it easier to combine modules together and match the module to the
string used in the import call:

module "jquery" {
}

module "foo" {
    import $ from "jquery";
}

This means that "foo" will get a handle on the "jquery" module. By
using string IDs even in the "module {}" part, it makes it easier to
match the references to the module provider, particularly for combined
files.

-----------
4) export call
-----------

There was mention in "simpler, sweeter syntax for modules" thread by
Dave that maybe with syntax like this:
import $ from "jquery.js";

that having a way to export a function may not be needed.

However, I still feel some sort of "export call" is needed, or some
way to export a function as the module value, to give parity with the
runtime calls:

System.load('jquery.js', function ($) { });

It would be awkward to do this:

System.load('jquery.js', function (something) { var $ = something.$; });

-----------
5) Compile time linking
-----------

There is a tension between the runtime calls like System.load and the
compile time linking of the module/import syntax. The runtime
capabilities cannot be removed. However, I believe it would simplify
the story for an end user if the the compile time linking is removed.

While the compile time linking may give some kinds of type
checks/export name checking, it is only one level deep, it does not
help with this sort of checking:

//Compile time checking can make sure
//'jquery.js does export a $
import $ from 'jquery.js';

//However, it cannot help check if foo is
//a real property
$.foo();

Similar story for prototypical properties on constructor functions.

New possibilities open up if this the compile time stuff is removed,
and I believe it simplifies the end user's day-to-day interaction with
modules (more below).

-----------
6) Import syntax
-----------

If the compile time linking/checking is removed, module referencing
gets simpler. No more import stuff, just use "from" to indicate the
value is fetched from a module:

let $ from "jquery";
let {search, fetch} from "service";
let {*} from "math";

Normal var/let and destructuring is used. The only new thing is "from"
which indicates a module source. It only allow a string literal for
the module name.

Not sure if that last * example is valid, but maybe allow it to be
valid to support the "import *" cases. If it is untenable, then throw
it out. Node and AMD users have done just fine without having *.

-----------
7) Object destructuring
-----------
Not module-specific, but it comes up in modules.  In the "simpler,
sweeter syntax for modules", Brendan wrapped up that mini-thread it up
by it just needs to be documented and taught, but I cannot help
calling it out again, particularly since I do not post very often to
this list.

No need to respond to this item, I just want to put my voice behind
the backwards-ness of:

let { draw: drawWidget } = widget;

It will always stick out because object literals are constructed the
other way, and object literals show up in the code next to these
destructuring calls. Even if you know the rules, scanning code always
requires a mental discontinuity jump to parse out what is going on.
Yes, there is logic behind the way it is, but it scans wrong, takes
the developer out of the flow. Sorry, had to get that out.

========
Proposal
========

Here is how module syntax might look given the feedback above:

module "foo" {
    let $ from "jquery",
        {search, fetch} from "service";

    //Use return to specify the exports, or optionally
    //use exports.foo = to assign values to properties
    //on an exports object, for use in circular dependencies
    return function () {};
}

The module "foo" {} wrapper would *not* be needed in a file that is
just declaring a module (think how node modules are constructed
today). It is just for inlined module declarations, like when a bunch
of modules are combined together.

For the unwrapped case, it means allowing "return" for unwrapped
modules in a place that normally has not been allowed. Hopefully that
can be worked out if a module is in play and not a Program (I am not a
grammarian, may have that wrong).

To allow existing libraries to work in non ES.next and ES.next
environments, perhaps also allow using an API form of the above:

module("foo", function (from, exports) {
    var $ = from("jquery"),
        service = from("service"),
        search = service.search,
        fetch = service.fetch;

    //Use return to specify the exports, or optionally
    //use exports.foo = to assign values to properties
    //on an exports object, for use in circular dependencies
    return function () {};
});

If "module" does not work for an API name, this last form is close
enough that the AMD "define" could be used instead, along with
swapping "require" for "from". But that is a minor naming bikeshed. I
would try to match the ES syntax names if possible.

The main point is to have an API that could be runtime-checked for
libraries that want to live in both the old and new world.

James


More information about the es-discuss mailing list