ModuleImport

Jussi Kalliokoski jussi.kalliokoski at gmail.com
Fri Jul 4 00:19:58 PDT 2014


On Thu, Jul 3, 2014 at 9:05 PM, Brendan Eich <brendan at mozilla.org> wrote:

> Jussi Kalliokoski wrote:
>
>> So sometimes someone can need it, so we must have good support? Is that
>> how we operate these days?
>>
>
> Cool down a minute :-|.


Heh, the internet is a funny place when it comes to interpreting emotion;
I've actually been very calm the whole time. ;D


> JS is a mature language on a big rich-and-messy evolving platform-set
> (browser JS, Node.js, other embeddings). We don't preach "only majority use
> cases" or "there should be only one way to do it" -- more TIMTOWDI or
> TimToady Bicarbonate:
>
> http://en.wikipedia.org/wiki/There%27s_more_than_one_way_to_do_it
>
> JS systems start small and grow. Modules often merge, split, merge again.
> Cycles happen in the large. ES6 modules address them, they were always a
> design goal among several goals.
>

I'm well aware, to me it looks like cycles are the defining feature of the
module system on which other features have been built on. The reason this
is a problem is because the requirement of cyclic dependencies not only
complicates the API surface, reasoning about it and implementations, it
also excludes a lot of features.

For example, if we look at the problem of optional dependencies (which,
btw, unlike cyclic dependencies are an extremely common corner case,
especially on the web, and can't really be refactored away) is not that you
can't load things dynamically, because you can, but in the fact that
importing a module is async but initializing and declaring is not, it's
static to support mutable bindings magic (that are required for transitive
cyclic dependencies) and compile-time errors.

Now, let's have a hypothetical change to the module system. Let's say that
we allow only exporting one thing, and that one thing can be any value.
When you import it, it's like assigning a variable, except that resolving
the value you are assigning to is done async at compile time. Like:

// somewhere.js

export {
  something: function something () {}
};

// doSomethingElse.js
import { something } from "somewhere";
export function doSomethingElse () {};

Benefit #1: No module meta object crap, the only new concept needed to
understand this is compile-time prefetching.
Benefit #2: No special destructuring syntax (since you're doing normal
destructuring on a normal value).

Cool, but we broke cyclic dependencies without fixing optional dependencies:

// optional dependency here
var fasterAdd = System.import("fasterAdd");
var basicAdd = function (a, b) {
  return a + b;
};

// because there's no async initialize, we have to impose an async
interface for an otherwise sync operation
export function add (a, b) {
  return fasterAdd
    .catch( => basicAdd )
    .then( (addMethod) => addMethod(a, b) );
};

However, with this design, we can allow exporting a promise of what we want
to export, thus deferring the import process until that promise is resolved
or rejected:

var basicAdd = function (a, b) {
  return a + b;
};

export System.import("fasterAdd")
  .catch( => basicAdd );

And there you have it, voilá.

Benefit #3: See #1, if you don't want to, you don't even have to comprehend
compile-time prefetching anymore. It's just optimization sugar.
Benefit #4: No need to impose async interfaces for inherently sync
operations just because the initialize phase is not async while loading is.
Benefit #5: You can do stuff like async feature detects in the initialize
phase, something that is completely broken in existing module systems (you
*can* do this with RequireJS through some effort, but I'm not sure it's
officially supported or part of AMD).
Benefit #6: High compatibility with existing module systems, including edge
cases, providing a solid foundation for transpiling "legacy" modules to the
new system. See some sketches I made yesterday when playing around with the
idea: https://gist.github.com/jussi-kalliokoski/6e0bf476760d254e5465
(includes an example of how you could implement localforage if the platform
dependencies were provided as modules).
Benefit #7: Addresses the issue of libraries depending on more than just JS:

import {loadImages, importStylesheets} from "fancy-loader";

var myModule = { ... };

export Promise.all([
  loadImages(["foo.jpg", "bar.png"]),
  importStylesheets(["style.css"])
]).then( => myModule );

There's probably more that didn't come to my mind. So, this would support
pretty much all the features of the existing solutions and more. Just not
cyclic dependencies. At all.

Now, we could of course try to add this as an afterthought to the current
design by letting individual exports be promises, but in order to preserve
transitive cyclic dependencies in that case, you'd have to wait for the
promise of that to resolve when the importing module accesses the imported
binding, not only causing execution to yield to the event loop unexpectedly
(which is what we're trying to avoid with async functions) but also makes
it transitive only most of the time, ending up with cyclic dependencies
that sometimes work maybe if things happen in a specific order. It would be
even worse than CJS cyclic dependencies support because unlike CJS it'd be
extremely vulnerable to async race conditions.

I don't know about other people, but I'd rather receive an outright error
for making cyclic dependencies than it being a fragile little beast that
does what you want if you don't do async stuff or if the right stars align.
Evolution just doesn't cut it if the foundation is broken. Unless we're
talking about the part where the weak diminish into non-existence, coming
to haunt us every now and then in old literature.

Cheers,
Jussi
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20140704/8d0d5889/attachment-0001.html>


More information about the es-discuss mailing list