ES Modules: suggestions for improvement

Claus Reinke claus.reinke at talk21.com
Tue Jul 24 13:11:23 PDT 2012


[I've elided some points and comments: I was trying to summarize 
 what seemed to me the core issues in this discussion; if my summary 
 was unclear, it won't help to add more text; if my summary was clear, 
 but the disagreements persist, adding more text won't help, either]

>>    Here I've come around to Isaac's opinion that 'import *' is a
>>    step too far. Previously, I said this is a convenient bad habit
>>    that might be left to linters. But that was based on experience
>>    with statically typed languages, where modules and their
>>    import/export interfaces could still be analyzed in separation.
>>
>>    In ES, that is not the case: if 'System.set' and 'import *' are
>>    combined, humans and tools would have to *run* dependencies
>>    to discover the import interface. That makes it impossible to
>>    analyze/understand such modules in separation, statically.
> 
> This is not correct.  You can look at a single module in isolation,
> and learn exactly the same things about its interface that you can in
> Haskell, for example.

Haskell isn't a good role model wrt module systems - the main
design goal there was simplicity, so it doesn't use advanced 
module system ideas (at least not in the standard module system). 
Also, some good aspects have disappeared, and some aspects 
haven't quite scaled up with the increased use. 

One thing that disappeared (because it wasn't done well) was 
interface files, which allowed to develop modules wrt module 
interfaces rather than module implementations. So, yes, Haskell 
suffers from a combination of 'import * from M' with no easy 
way to pin down M's expected export interface.

Standard ML, and variants that support higher-order or even
first-class functors (parameterized modules), might be more
interesting in this context. Even when it can't be statically
(before running the module-level code) determined which 
module will provide the imports, one can pin down which 
interface that module will provide. So one can understand 
each module in isolation, with the import and export interfaces 
acting as boundaries.

But we can stay in ES6 for this discussion - consider

<script>
System.set('X',(Math.random() > 0.5) ? {x:"hi"} : {u:"oops"};
</script>

<script>
module M1 {
import * from X;
console.log(x.length);
}
module M2 {
import {x} from X;
console.log(x.length);
}
</script>

I cannot look at 'M1' and know whether or not 'x' is bound,
because the import interface is unspecified. So I'd have to
look at 'X' and, in this case, I'd have to run 'X' before I could
tell whether 'x' in 'M1' is going to be defined. 

In contrast, I can look at 'M2' and know, because the import 
interface is specified, that, if 'X' is accepted as dependency 
for 'M2', then 'x' will be available. There might still be a
problem at runtime, but it will be outside 'M2', and it will be
about matching an export interface to an import interface,
not about static scoping in 'M2'.

(Such dynamic module aspects are another reason why one
wants to keep exported, imported, and local names separate.)

>>    In brief, in the context of a language as dynamic as JS, the
>> convenience of 'import *' is not worth the damage it does to
>> modular program understanding. Instead, we should ensure
>>    that import interfaces are clearly and statically defined.
> 
> I disagree.  Clearly and statically defined interfaces are a great
> thing for some software.  Other programs, be they scripts written 
> by middle school kids or dynamically-reflective towers of
> meta-programming, don't want or need them.  What's the 
> interface to `$`, in the face of jQuery plugins -- you can't tell, 
> statically.  But that doesn't mean plugins are a bad thing.

The question is not the export interface of '$', which can change
with every plugin or new release. The question is whether 
importers of '$' can isolate themselves from such changes by 
specifying an import interface. Any export interface that 
provides the import interface will do.

Of course, it is not just handy but a pragmatic necessity not 
having to write out 'standard' imports, but as with physical
'constants', things can go awry if 'standards' change. It would
be great if I could abstract over import interfaces, so that I
don't have to write them out on every import declaration.

One way to do this is via tools: for Haskell, I had a Vim plugin
that would allow me to write an unqualified variable, and
then have the plugin search the available module exports to
make suggestions about imports, adding the selected imports
and qualifiers. So I'd be free of worrying about writing import 
declarations, but my code would have the import interfaces 
documented.

Another way would use module loaders: the default System
loader already inserts implicit imports for some standard
modules, so it could insert those with explicit import lists.

And I could have a project-specific loader adding import
declarations for my 'standard' project imports.

And for school kids, one could have a course-specific loader
inserting course-specific import declarations. One could
even follow DrScheme in have level-specific 'standard'
imports. Or have a standard set of 'play around' imports.

I could be wrong, of course, but I think that there are other
(and better) solutions to the issues we are tempted to
address with 'import *'.

>>    import .. from 'loader!resource'
> 
> Dave and I have been talking about this, and fortunately it doesn't
> require changing the core elements of the module system -- it just
> means making the `System` loader somewhat more configurable at
> runtime.  Then you'd be able to specify what the 'text' loader should
> do, and it would automatically hand 'text!resource' off to that
> loader, using the existing module loaders mechanism.  This wouldn't
> reduce any of the benefits we get, as Dave listed earlier, but would
> allow us to express the sorts of things you can do in AMD with loader
> plugins.

Great!-) From what I could see of the discussion, this should
remove the main technical obstacles raised against upgrading
to ES6 modules. 

The translate hook should allow for things like Coffeescript 
or Streamline. The fetch hook ought to help with alternate 
sources (CDN with local fallback). The resolve hook ought to 
allow something like the RequireJS config (mapping abstract
module names to concrete resources in a central position).

Looking forward to ES6 modules,
Claus
 


More information about the es-discuss mailing list