Modules

Eloquent JavaScript Chapter 10 の読書ノート。

巨大な泥団子はすべてがくっついていて、部分を切り取ろうとすると全体がバラバラになり手が汚れる。プログラムにもそういうものがある。

Modules

  • モジュールとは、プログラムの部分であって、他のモジュールがその機能を使用できるようにインターフェイスを備えるものだ。

  • モジュールのインターフェイスもオブジェクトのそれと多くの共通点がある。モジュールの一部は外部に公開し、残りは非公開にする。するとシステムはレゴブロックのようにモジュールの組み合わせになる。

  • モジュール間の関係を 依存関係 という。あるモジュールが他のモジュールの断片を必要とする場合、そのモジュールに依存しているという。

  • モジュールを分けるためには、それぞれにプライベートスコープが必要になるのだが、 JavaScript のコードを別のファイルに分けただけではダメだ。ファイルは同じ大域名前空間を共有している。また、依存関係の構造も明瞭になるわけではない。

  • プログラムに適したモジュール構造を設計するのは難しい。

Packages

  • いったんコードを複製し始めると、オリジナルとその複製(複数あるだろう)の維持に時間と労力を浪費することになる。そこでパッケージの出番となる。

  • パッケージ とは、配布(コピーやインストール)が可能なコードの塊だ。

    • パッケージはモジュールで構成されていて、依存する外部パッケージが何であるかという情報を持っている。

    • パッケージには通常、それが何をするものなのかを説明する文書が付属する。これにより、パッケージの著者ではない人でも利用できるようになる。

  • パッケージに問題が発見されたり、新しい機能が追加されたりすると、それは更新されるという。更新すれば、そのパッケージに依存しているプログラムは、新しいバージョンにアップグレードできる。

  • JavaScript の世界ではパッケージを NPM <https://npmjs.org> というもので管理する。

    • NPM はパッケージのダウンロードサービスと、インストール管理を支援するプログラムの二つの要素がある。

    • 本書執筆時点で NPM には 50 万以上のパッケージがある。一般公開されている有用なパッケージのほとんどは NPM で見つかる。

      • 例えば前章で作成したような INI ファイル読み込み機能を提供するパッケージもある。

Todo

第 20 章で npm コマンドラインプログラムを使ってパッケージをローカルにインストールする方法を習得する。

  • 多くのパッケージは、他の人が使用することを明示的に許可するライセンスの下で公開されている。NPM のほとんどのコードはこのようなライセンスで公開されている。

  • パッケージに依存するコードを同じライセンスで公開することを要求するライセンスもある。他のライセンスはそれほど厳しくなく、コードを配布する際にそのコードと一緒にライセンスを保持することを要求するだけだ。JavaScript のコミュニティーでは、ほとんどが後者のライセンスだ。

  • 他人のパッケージを使用する際には、そのライセンスを確認することだ。

Improvised modules

  • 2015 年まで、JavaScript言語にはモジュールシステムがなかった。そこでモジュールを所望する人々が独自のモジュールシステムを設計した。

  • 次のコードは曜日名と対応する数字を変換する機能があるモジュールだ:

    const weekDay = function() {
        const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
                       "Thursday", "Friday", "Saturday"];
    
        return {
            name(number) { return names[number]; },
            number(name) { return names.indexOf(name); }
        };
    }();
    
    • インターフェイスは weekDay.nameweekDay.number の二つ。

    • ローカル変数はすぐに呼び出される関数式のスコープにあるため、外部からは見えない。

  • このモジュールはある程度は分離されているものの、依存関係が宣言されていない。自身のインターフェイスを大域名前空間に入れており、依存するものに対しては、同じようにそれらも大域名前空間にあることを想定している。

  • この方式は現在ではほとんど使われていない。

  • 依存関係をコードの一部にするには、依存関係のロードを制御する必要がある。それには文字列を JavaScript のコードとして実行する機能が必要だ。

Evaluating data as code

  • 特別な演算子 eval を使うと、与えられた文字列を現在のスコープ内で実行することになる。よその言語の同等の機能と同様に、これを使うのは良くない。いろいろなものを破壊する。

  • Function コンストラクターを使う方法は eval よりは怖くない。これは , 区切りの引数リストからなる文字列と、関数本体からなる文字列を受け取ってコード化し、結果を返すというものだ。

    let plusOne = Function("n", "return n + 1;");
    console.assert(plusOne(4) == 5);
    
  • Function を利用してモジュールを構成していく。

CommonJS

JavaScript モジュールを追加するのに最も広く使われている方法は CommonJS モジュールというものだ。Node.js はこれを採用しており、NPM のパッケージのほとんどで採用されているシステムだ。

  • CommonJS モジュールでは require という機能が重要だ。これを依存モジュールの名前を指定して呼び出すと、当該モジュールがロードされて、そのインターフェイスを返す。

  • ローダーがモジュールコードを関数にラップするので、モジュールは固有のローカススコープを自動的に得る。あとは require を呼び出して依存関係にアクセスして、インターフェイスをオブジェクト exports に置くだけだ。

依存モジュールが二つあるモジュールの作成例が示されている。

const ordinal = require("ordinal");
const {days, months} = require("date-names");

exports.formatDate = function(date, format) {
    return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => {
        if (tag == "YYYY") return date.getFullYear();
        if (tag == "M") return date.getMonth();
        if (tag == "MMMM") return months[date.getMonth()];
        if (tag == "D") return date.getDate();
        if (tag == "Do") return ordinal(date.getDate());
        if (tag == "dddd") return days[date.getDay()];
    });
};
  • この自作モジュールは外部パッケージ ordinaldate-names に依存している。

    • 前者は 1st2nd のような序数を示す文字列を数に変換するのに使う。

    • 後者は曜日名や月名に対する英単語を得るのに使う。

  • この自作モジュールがエクスポート(外部に公開、提供)するのは関数 formatDate だけだ。

  • パッケージ ordinal のインターフェイスは関数一つ。ここでは定数 ordinal として参照する。

  • パッケージ date-names のインターフェイスはオブジェクト一つであって、そこには曜日名や月名を表す英単語の配列がある。ここでは定数 daysmonths として参照する。

  • この自作にモジュールが公開するインターフェイスとして、オブジェクト exportsformatDate を追加する。

  • この自作モジュールを使うには次のようにする:

    const {formatDate} = require("./format-date");
    console.log(formatDate(new Date(2017, 9, 13), "dddd the Do")); // → Friday the 13th
    
  • 関数 require をもっとも最小の形式で定義するには本書のコード (p. 178) のようにする。

    • 関数 readFile が標準 JavaScript に存在しないものであることに注意。 Node.js をインストールしてからこの章を読む必要があった。

  • 関数 require はロード済みモジュールをキャッシュする。

CommonJS モジュールには癖がある。モジュールシステムが空のインターフェイス用オブジェクト exports を作成してくれるにもかかわらず、exports を上書きすることでどんな値でも置き換えることが可能だというものだ。多くのモジュールでは、インターフェイスオブジェクトの代わりに単一の値をエクスポートするために、この置換の手法が横行している。

  • 生成されたラッパー関数の引数として require, exports, module を定義する(呼び出すときに適切な値を渡す)ことで、ローダーはこれらの変数がモジュールのスコープで利用可能であることを保証する。

  • require に与えられた文字列が実際のファイル名やウェブアドレスに変換される方法はシステムによって異なる。

    • 文字列が ./../ で始まっている場合は、一般に現在のモジュールとの相対パスとして処理される。

    • 名前が相対的でない場合、Node.js はその名前でインストールされたパッケージを探す。本章のサンプルコードでは、このような名前は NPM パッケージを参照していると解釈している。

  • 前章のような INI ファイルの読み取り機能を自作する代わりに、NPM にあるものを使うことができる:

    const {parse} = require("ini");
    console.log(parse("x = 10\ny = 20")); // → {x: "10", y: "20"}
    

ECMAScript modules

2015年 からの JavaScript 標準では CommonJS とは異なるモジュールシステムを導入した。それは通常 ES モジュールと呼ばれる。

  • ES は ECMAScriptの略。

  • 依存関係やインターフェイスといった主要な概念は CommonJS と変わらないまま、細部が異なる。

  • 表記法が言語に統合された。依存関係にアクセスするために関数を呼び出すのではなく、特別なキーワード import を使う。

  • キーワード export はモジュール要素をエクスポートするのに使う。このキーワードは関数、クラス、変数各種の前に現れることがある。

    import ordinal from "ordinal";
    import {days, months} from "date-names";
    export function formatDate(date, format) { /* ... */ }
    
  • ES モジュールのインターフェイスは単一の値ではなく、名前のある変数の集合だ。

  • export default を使うと、エクスポートする要素を指定できる。

    export default ["Winter", "Spring", "Summer", "Autumn"];
    
  • Python のようにキーワード as を用いてインポート名を指定することができる。

    import {days as dayNames} from "date-names";
    
  • ES モジュールのインポートは、スクリプトの実行を開始する前に起こるという違いがある。import 文を関数やブロックの中に記述することはできない。

Building and bundling

JavaScript のコードがあっても、それが元々 JavaScript で書かれたものであるとは限らない。

Module design

  • モジュールのデザインには、使いやすさという側面もある。

  • 標準的な機能や広く使われているパッケージがなくても、単純なデータ構造を使い、単一の集中的な作業を行うことで、モジュールを予測可能なものにできる。

  • 副作用のある複雑な動作をする大きなモジュールよりも、値を計算する集中的なモジュールの方が、より幅広いプログラムに適用できる。

  • 関数でできることは関数を使え。

  • 配列で十分な場合は配列を使え。

  • 合成し易さを考慮して設計したい場合は、他の人がどのようなデータ構造を使用しているかを調べ、可能であればその例に従え。

Summary

  • モジュールは、大規模なプログラムに構造を与えるために、コードを明確なインターフェイスと依存関係を持つ断片に分離する。

  • インターフェースとは、他のモジュールから見えるモジュールの部分であり、依存関係とは、そのモジュールが利用する他のモジュールだ。

  • 歴史的に JavaScript はモジュールシステムを提供していなかったので、CommonJS システムはその上に構築された。その後、ある時点で組み込みのシステムが導入されたが、現在は CommonJS とよく共存している。

  • パッケージとは、単体で配布可能なコードの塊だ。

  • NPM は JavaScript パッケージのリポジトリーだ。

Exercises

A modular robot

第 7 章では次の定数、変数、関数、クラスを導入した:

  • roads

  • buildGraph

  • roadGraph

  • VillageState

  • runRobot

  • randomPick

  • randomRobot

  • mailRoute

  • routeRobot

  • findRoute

  • goalOrientedRobot

問題 このプロジェクトをモジュラープログラムとして書くとしたら、どのようなモジュールを作るか。また、どのモジュールがどのモジュールに依存しているだろうか。そのインターフェイスはどのようになっているだろうか。

NPM であらかじめ書かれたものはどのようなものがあるか。

解答 まず依存関係をまとめる。Graphviz などで図式化するといい:

  • 定数 roads は独立して存在している。

  • 関数 buildGraph は独立して存在している。

  • 定数 roadGraph は次のものに依存している:

    • roads

    • buildGraph

  • クラス VillageStateroadGraph に依存している。

  • 関数 runRobot は引数のインターフェイスに依存している。

  • 関数 randomPick は標準モジュール Math に依存している。

  • 関数 randomRobot は次のものに依存している:

    • 引数 state のインターフェイス

    • roadGraph

    • randomPick

  • クラス VillageState.random のため randomPick に依存する。

  • 定数 mailRoute は独立して存在しているが、値は roads から決まる。

  • 関数 routeRobotmailRoute に依存している。

  • 関数 findRoute は引数のインターフェイスに依存する。

  • 関数 goalOrientedRobot は次のものに依存している:

    • roadGraph

    • findRoute

次のようにモジュール群を編成できる:

モジュール

内容

~に依存する

mailRoute.js

mailRoute

なし

graph.js

buildGraph

なし

randomPick.js

randomPick

なし

roadGraph.js

roadGraph, roads

graph.js

randomRobot.js

randomRobot

roadGraph.js, randomPick.js

routeRobot.js

routeRobot

mailRoute.js

goalOrientedRobot.js

goalOrientedRobot, findRoute

roadGraph

runRobot.js

runRobot

なし

village.js

VillageState

roadGraph.js, randomPick.js

NPM は調べていないが、ありそうなのはランダムピック機能か。

Roads module

問題 第 7 章の例をもとに、道路の配列を格納し、道路を表すグラフデータ構造 roadGraph をエクスポートする CommonJS モジュールを書け。

このモジュールは、グラフを構築するために使用される関数 buildGraph をエクスポートするモジュール ./graph に依存するものとする。この関数は、二要素(道路の始点と終点)の配列を引数に取る。

解答 前問のようにモジュール群を編成するとして:

 const {buildGraph} = require("./graph");

 const roads = [
    "Alice's House-Bob's House", "Alice's House-Cabin",
    "Alice's House-Post Office", "Bob's House-Town Hall",
    "Daria's House-Ernie's House", "Daria's House-Town Hall",
    "Ernie's House-Grete's House", "Grete's House-Farm",
    "Grete's House-Shop", "Marketplace-Farm",
    "Marketplace-Post Office", "Marketplace-Shop",
    "Marketplace-Town Hall", "Shop-Town Hall"
];

 exports.roadGraph = buildGraph(roads);

なお、関数 require は Node.js のものを想定している。

Circular dependencies

循環依存 とは、モジュール A が B に依存し、B も直接または間接的に A に依存している状況だ。多くのモジュールシステムでは単純にこれを禁じている。このようなモジュールをロードする順序をどのように選択しても、実行前に各モジュールの依存関係がロードされていることを確認できないからだ。

CommonJS のモジュールでは、限られた形での周期的な依存関係を認めている。モジュールがデフォルトの exports オブジェクトを置き換えず、ロードが完了するまでお互いのインターフェイスにアクセスしない限り、周期的な依存関係は問題にならない。

問題 この章の序盤で与えた関数 require は、この種の依存関係の循環をサポートしている。どのように循環を処理しているか。循環内のモジュールがデフォルトの exports オブジェクトを置き換えた場合、何が問題になるか。

解答 require.cache があるおかげで、二度目以降のロードを無視する。C/C++でいうところのインクルードガード(この技術は古いが)のような働きをする。

以上