osiire’s blog

ふしぎなそふとやさん

Let’s module programming!

オブジェクト指向なら分かるけど、モジュールでどうやってある程度の規模のプログラムをするのか、全く想像つかない!そんな諸氏のためにモジュールプログラミングのテクニックを、レベル分けしながら解説してみたいと思います。

レベル1(基礎編)

アプリケーションで使う予定のデータや処理を大雑把にグループ分けして、それらに名前をつけて.mlファイルにします。個々の.mlファイルがモジュール、かつ、スコープ、かつ分割コンパイルの単位になるので、ある程度見通しがよくなります。モジュール同士の連携は、基本的にモジュール名を指定するだけ。後はその.mlファイルにシグネチャーを付けるもよし、つけなくても可。最後にmain.mlファイルを作って、そこからアプリケーションを起動するようにすればOK。とても簡単です。しかし、実を言うと、凝った作りのライブラリでもない限り、ほとんどこれで事足ります。大事なので二回言います。ほとんどこれで事足ります

レベル1.5(おまけ)

ある特定の2個のデータが相互参照していて、.mlファイルに分けられないと悩む初心者がいます。大抵、その2個のデータが使っている、共通している型を別途下層の.mlファイルとして括り出せば解決します。本質的な再帰ではありません。(体験談)

レベル2(ちょっと慣れてきた感)

少しモジュールの扱いになれてくると、.mlファイルの中に外部には晒さないモジュールを定義して、内部的な処理も区分けしたくなります。慣れてきた証拠です。そうすると、段々名前空間も欲しくなってきます。そのような場合には、コンパイル時に-packオプションを使います。-packオプションで複数の.cmoを一つの.cmoにまとめると、複数のモジュールを一つのモジュール空間に押し込める事ができます。また、複数のモジュールをopenして使うと気持ち悪いという感覚がでてきたら、

module G = Graphics
module S = Set

のように略記を作ったりすると便利です。

レベル3(一歩踏み出す)

少し複雑なデータ構造や特殊な機能のために、やや高等なモジュールテクニックを応用できるようになると、幅が広がります。
例えば、

  • Ordシグネチャーとファンクターを使って独自のTreeモジュールを作る。
  • グラフ構造をEdgeとVertexの二つのモジュールに分けて、それらをwith type付きで相互再帰させてGraphモジュールを作る。
  • 標準ライブラリのSetモジュールを相互再帰して使う。
  • コンストラクタをprivate指定で公開する。
  • 簡単な幽霊型パラメーターを使って、型検査を強化する。

などです。出来上がったこれらの機能は外からはごく普通のモジュールに見えるので、局所的なテクニックであり、それゆえ簡単にアプリケーションに導入できます。こういう事例を経験してくると、モジュールがよく考えられたシステムだと思えてきます。

レベル4(シグネチャーを活用)

プログラム内でシグネチャーを活用できるようになってきたら、いよいよ本物です。よく使うシグネチャーを予め定義して使い回します。例えば、JaneStreetのCoreライブラリには、次のようなシグネチャーが定義されています。

module type S = sig
  type stringable
  val of_string : string -> stringable
  val to_string : stringable -> string
end

このシグネチャーは、Stringableと名付けられ、プログラム内のあちこちでincudeされています。ファンクターの引数にもなっています。Java的に言うと、ちょうどStringableというinterfaceを定義して、別のintefaceを定義する時に利用したり、interface経由でインスタンスを操作したりする感じです。Coreライブラリでは、他にもHashableとかMonadなど10種類程度(細かく分けるともっとたくさん)のシグネチャーが定義されています。このような大域的に使えるシグネチャーは、アプリケーション内に統一感を生みだします。この統一感は複雑なアプリケーションにとって不可欠なものです。ちなみに、Coreだけでなく、ファイル同期ソフトで有名なUnisonでも類似テクニックの活用が見て取れます。

レベル5(第一級モジュールの活用)

OCaml3.12からモジュールが第一級になりました。これによって、幾つかのテクニックが新しく使えるようになりました。

  • 外部環境に依存したモジュールの入れ替え
  • 動的なプラグイン

こいつらは解説しているとそれだけで一エントリーできてしまうので、詳細は省略。こちらに詳しい解説があります。http://www.math.nagoya-u.ac.jp/~garrigue/papers/ocamlum2010.pdf

レベル6(継承/サブタイプを活用)

このレベルは私もまだ実験段階です。でもamthingでは取り入れている手法です。モジュールが第一級になって、引数として受け取ったモジュールに機能を追加して戻り値にするといった破天荒な関数を記述できるようになりました。これはある意味インスタンス指向のモジュールプログラミングであり、実装継承の利点を活用するものです。さらに、拡張されたモジュールを関数に渡す場合でも、そのモジュールが指定された型を含んでいれば型チェックが通ります。こういった、継承/サブタイプの連携は、大規模な共通化(例えばGUIツールキットのコンポーネントとか)に有効だと考えています。

レベル7(GADT)

コンパイラによるネイティブサポートを待ちましょう。たぶんもうすぐです。

以上、モジュールプログラミングのテクニックをレベル分けしながら俯瞰してみました。これでモジュールを恐れず使ってもらえるようになれば幸いです。