Composition of Uncoordinated Working Sets of Modules

David Herman dherman at
Mon Jun 7 15:23:27 PDT 2010

> It would be
> good for this to be expressed in one of the examples, and
> for it to be clarified in the description of semantics that
> every script is also an anonymous module from which the
> exports are only accessible through the lexical scope
> "shadowing" (I assume) and by being bound to a module
> through a "load" expression.

This doesn't sound quite right-- in the terminology we used, scripts are not modules. An application is composed of a sequence scripts, which are like module bodies but do not contain exports. Each script's bindings are in scope for all subsequent scripts. By contrast, the target of "load" is the body of a module, which can export bindings.

> This is a point that Ihab clarified for me yesterday
> evening that merits bold and emphasis: loaded modules are
> not singletons.

Yes, I think something we need to do is write out some more material explaining the proposal in more tutorial fashion. The examples page was a start, but clearly not enough.

> You do this to avoid having to compare
> MRL's for equivalence, particularly to avoid having to
> define equivalence given the potential abundance of edge
> cases.

Yes, as well as the fact that even the bit-for-bit same URL can deliver different bits from moment to moment. So module loading really is effectful (albeit at compile time), in the sense that it performs arbitrary Internet I/O. In lieu of requiring programmers to learn rules about when two references to modules are referring to the same memoized instance and when the instance is loaded and evaluated, simple modules make all this explicit and under the programmer's control.

> It would also be good for there to be a way to bind $ without binding
> a module.
>    const A = load("aQuery.js").$;
>    const B = load("bQuery.js").$;

There are a couple reasons why I think I'd avoid this kind of thing: for one, it means that |load| -- which indicates a /compile-time/ operation -- can now be arbitrarily nested in a program instead of just at top level. Also, loading is a fairly heavyweight operation, and since it doesn't memoize, you could very easily end up with accidental duplication.

As Sam says, you can write almost the same thing via dynamic loading:

    const A = ModuleLoader.current.loadModule("aQuery.js").$;
    const B = ModuleLoader.current.loadModule("bQuery.js").$;

or a little more conveniently:

    function load(ml, mrl) {
        return ml.loadModule(mrl);

    const ml = ModuleLoader.current;

    const A = load(ml, "aQuery.js").$;
    const B = load(ml, "bQuery.js").$;

The main difference from what I think you intended is that this would do the loading dynamically.

>> It's possible to use the module loader API to do this,
>> slightly more verbosely.   But why?  If you say:
>> module A = load "aQuery.js";
>> then A.$ is already available for use in expression
>> contexts.
> I can make the same argument about "import *".  If I "import
> A", I can access its contents as "A.$".  To permit
> destructing on all import expressions would be consistent
> philosophically.

I don't follow your reasoning-- import A.* is a convenience form to bind the exports of A as local variables. It serves a very different purpose.

Local, nested module loading could either mean static loading, which I contend would be confusing and error-prone, or dynamic loading, which is already available via the dynamic loading API.

>> Your example might point to a need to augment the module
>> loader api with information on 'load' calls specifying
>> what module the 'load' occurs in.
> Exactly.  The Narwhal loader receives an id and a baseId on
> from require(id) calls.  Each module gets a fresh "require"
> effectively bound on the baseId.  I think that the loader
> handler needs to receive the base MRL as an argument or part
> of the request object.

Yes, I agree. That was an oversight-- thanks for bringing it up.

> Another thing that Ihab clarified which merits a full
> section on the wiki is the dynamic scoping of lexical module
> names.

I've said it before: it's not dynamic scoping. It's static, lexical, compile-time scoping. Dynamic scoping necessarily involves dynamically determining the binding of a variable. There's nothing of the sort happening here; it's all compile-time.

> This is something I have not considered.  It would be good
> to do a write-up on what use-cases you have in mind for this
> feature.

Yes, we should definitely do that. Two important use cases are 1) standard libraries, which would be shared as global module bindings in a standard module loader, and 2) mutually recursive modules, which need to agree on what they call one another.

> I'm going to mull the implications, but one for
> sure, is that it is necessary to buy a whole package even if
> you only want a single function from it.

True. I don't think it's reasonable to try to solve the more intricate problems of partial or on-demand loading of modules. I think people are still experimenting with this in the wild, and IMO it's premature to try to solve this in Harmony. With dynamic loading, people can continue experimenting with different approaches. And especially when you consider the fact that on-demand loading means the semantics is doing network I/O behind your back and lazy evaluation of arbitrary JS code, it becomes a very difficult programming model. As I've said before, laziness and effects don't mix (ask any Haskellite! ;).

> * load handlers need to receive the MRL of the declaring
>  module so they can resolve relative MRLs.  This one's
>  important.


> * there should be cleaner syntax for destructuring a loaded
>  module.  Preferably whether a module is rebound or loaded
>  should be orthogonal to the destructuring syntax.  Sugar.

I'm not sure I know what you're looking for here.

> * the community should be called upon to weigh in on whether
>  "import *" should be supported, and we should frame the
>  question with education on the full implications of the
>  trade-off.

A few thoughts:

1. I'd rather have a module system without import A.* then no module system at all.

2. Community discussion of the issue is, of course, fine -- that's what es-discuss is here for.

3. IMO, it'd be a big mistake to eliminate import A.*. It's not hard to avoid it, proscribe it in coding standards, or even reject it from lint tools. But withholding it from the language eliminates a real convenience for simple scripting, which continues to be an important use case for ES. And I don't see the proscription providing enough value.

The hazard of import A.* is that a new version of A may introduce new bindings that conflict with another module's bindings. In that case, code in the wild will start failing because it gets a compile-time conflict that isn't resolved. Another cost is that people would be more likely to use import A.* when A is some widely used library (including the builtins). This adds /some/ pressure for library writers to be more conservative about introducing new bindings.

But the failure mode for all of these is an early, compile-time error; in practice, production systems would pin particular versions of libraries rather than downloading from a 3rd-party server; and generally production systems would probably avoid it anyway. And that's not what it's there for. It's there for quick scripts, rapid prototyping, etc.

> * we should consider a way to link one loader to another,
>  such that a loader, for example a package loader, can be
>  mapped responsibility for all modules in a subtree of the
>  MRL name space without having to communicate exclusively
>  in source strings.

IIUC, this might already be achievable with the load hook (the function passed to the ModuleLoader constructor). The latter can make whatever decisions it wants about how to handle any given MRL. And it avoids having to over-specify MRL's. (We may end up needing to specify more of MRL's anyway, but I'd prefer to do no more than necessary.)


More information about the es-discuss mailing list