Skip to content

Instantly share code, notes, and snippets.

@domenic
Last active August 29, 2015 14:02
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save domenic/2230a7195fa0de31a227 to your computer and use it in GitHub Desktop.
Save domenic/2230a7195fa0de31a227 to your computer and use it in GitHub Desktop.
Practical implications of removing `module x from "y"`

Practical Implications of Removing module x from "y"

Background

According to this gist, the modules break-out session at TC39 (a subset of the larger committee) decided to remove the

module x from "y";

syntactic form, citing confusion versus import x from "y" (which is sugar for import { default as x } from "y"). Let's call these two forms "module instance object form" (or MIO form) and "default import form".

Alternatives

The MIO form was mostly useful when importing from a module with many exports:

// Now no longer possible:
module m from "m";
m.x(); m.y(); m.z();

Instead, we now can do:

import { x, y, z } from "m";
x(); y(); z();

or

import "m";
const m = System.get("m");
m.x(); m.y(); m.z();

The latter has a large disadvantage of removing all compile-time checking of the imported bindings. So if the last line was typoed to include m.zz(), this would no longer be a compile-time error. This drawback also applies to the old form. However, the old form still is slightly more statically verifiable, as it can be guaranteed robust against tampering with System.get (which misguided souls could do, for example, to implement a poor-man's normalize hook). The differences are minor, but interesting.

Practical Impact

I use several modules with many exports, despite the community propaganda surrounding the "single default export only!" school of thought. These include modules like Q, jQuery, Chai, Underscore/Lo-Dash, and Request. Take a look through npm's most-depended upon packages and check out how often they are multi-export. An additional large chunk comes from my usage of the Node standard library.

The Node standard library, in fact, has a good representative spread of modules:

  1. Modules like events which have only a single useful export, and probably should be using the default export form.
  2. Modules like querystring which are utility grab-bags that you often want only one or two exports from;
  3. Modules like crypto where sometimes you want a single item, and sometimes you want many items (e.g. someone getting random bytes, vs. someone doing cryptography).
  4. Modules like os or fs which you often want many exports from at the same time;

I'd encourage you to find other examples of each category; there are many.

For case 1, in ES6 default exports will be used.

For case 2, in ES6 the import { parse } from "querystring" form, or more likely import { parse as parseQS } from "qs" form, will make a good deal of sense.

For cases 3 and 4, things get tricky. Your alternative is now something like:

import {
  hostname as osHostname,
  type as osType,
  platform as osPlatform,
  arch as osArch,
  totalmem as osTotalmem,
  cpus as osCpus } from "os";

or

import "os";
const os = System.get("os");
// Now you can use `os.hostname()`, `os.type(),` etc.

Potential Author Reactions

The former possibility seems very unlikely to come into use, and we can probably ignore it. Depite the static verification benefits, it is simply too verbose to expect people to type. So let us turn our attention to the latter possibility.

Compare the latter possibility with const os = require("os"). It is really terrible in comparison, especially if you have to do it a few times in each file. So I don't think authors will want to do that either. What will they do instead?

On the consumer side, you can counteract this by putting aside ES6 modules and going back to CommonJS or AMD. It's important to remember that at this point ES6 modules are simply a third format alongside existing ones, and have to compete for authoring attention in the marketplace. Having committee backing provides a marketplace advantage, and they can use a "third way" effect to attract people who are disatisfied with the current CommonJS vs. AMD schism, but these are only potential advantages, and not realized ones yet. Note that one of the primary benefits of ES6 modules over existing solutions, namely static verification, is explicitly removed in the scenario under question. In fact, it only applies to case 2 (and sometimes 3) of the above list.

On the producer side, to alleviate your consumers of this pain, you can fall back to simple default exports. That is, if you are authoring the fs module (or Q, or jQuery, or Underscore, or Lo-Dash), you can author them not to use multiple exports, but instead to use the CommonJS/AMD-esque style of simply default-exporting a large object with many methods and properties. Or, of course, you can refuse to transition to ES6; the pressures there are different for library authors vs. app-builders, but you can imagine that the large portion of library authors who also use Underscore/Lo-Dash as a dependency would experience both negative pressures.

It's hard to say which way this will go, but it seems at least possible, and indeed somewhat likely, that this will remove motivation to do the "correct" thing for multi-export modules, and instead encourage authors to fall back to default-exports only. This is the smallest delta from existing authoring: you simply change module.exports = _ to export default _, instead of going and changing each _.foo = function (...) { ... } to export function foo(...) { ... }. And it gives consumers a single story: always use import x from "y", no matter what.

In this world, the static verification benefits of ES6 modules are completely removed; doing import { zip } from "underscore" will not work, and instead simply import _ from "underscore" combined with _.zip usage will prevail. This of course brings us back to the question of why people would switch from existing systems to ES6 modules, but let's assume they're locked in to that technology via e.g. tooling like ember-cli.

@locks
Copy link

locks commented Jun 8, 2014

Hi, quick question. If it's even possible, what would be the difference between doing

import {
  hostname as osHostname,
  type as osType,
  platform as osPlatform,
  arch as osArch,
  totalmem as osTotalmem,
  cpus as osCpus } from "os";

and

import { hostname as osHostname } from "os";
import { type as osType } from "os";
…

?

@mvolkmann
Copy link

Why can't this:
import m from "m"
mean the same as this used to mean?
module m from "m"

@caridy
Copy link

caridy commented Jun 8, 2014

@locks, they are equivalent.

@caridy
Copy link

caridy commented Jun 8, 2014

@mvolkmann, this is precisely why we kill it, it is confusing. and no, we don't want/need redundant APIs.

@briandipalma
Copy link

I would have little problem doing

import {
  hostname as osHostname,
  type as osType,
  platform as osPlatform,
  arch as osArch,
  totalmem as osTotalmem,
  cpus as osCpus } from "os";

It's not the prettiest but I'd want static verification.

I think it would be helpful if TC39 were to give different status ratings to each of the sub features of ES6 just like they will start doing in the post-ES6 process.

While I agree with Domenic's sentiments regarding these latest changes I also agree that the changes make sense. The future is much longer than the past so it's best to get it right instead of rushing something out.

@KidkArolis
Copy link

Why can't we have this?

  1. import mkdirp from "mkdirp" where mkdirp is a function
  2. import {parse} from "querystring"
  3. import crypto from "crypto" or import {randomBytes} from "crypto". And import request from "request" where request is now a function and import {get, post} from "request".
  4. import fs from "fs"

@spacepluk
Copy link

I must say at the beginning I found the module/import differences a bit confusing. Once I understood how they work, it just became annoying having to use a different syntax depending on how the module is written.

I think that killing the module m from "m" is a poor solution... and I'm starting to think that static verification is not worth the hassle for me. So I guess I'm in the group that is going back to node-style modules for the time being.

@boblauer
Copy link

Will import { parse } from "querystring" work if parse is a method on querystring's default export, e.g. export default = { parse: parse }?

Or is it to be used when parse is exported as a module property, e.g. export parse;

Or will it work in both scenarios?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment