4. モジュールとは何か?

../_images/modules1.png

インタラクティブシェルが使えることはしばしば動的プログラミング言語ではかなり重要な部分と考えられています。 あらゆるコードやプログラムのテストに役立ちます。大抵のErlang基本データ型はテキストエディタやファイルへの保存なしに使うことができます。 キーボードを捨てて、外でボールで遊んで一日を終えることができます。しかしそこで止まってしまっては、Erlangプログラマとしては終わってます。 コードは使われるためにどこかに保存しなければいけません!

そのためにモジュールが存在します。モジュールは1つの名前で1つのファイルに纏められたひとまとめの関数群です。 さらに、Erlangのすべての関数はモジュール内に定義されなければいけなません。 おそらく気がついていないでしょうが、あなたはすでにモジュールを使ってきました。 以前の章で紹介した hdtl といったBIFは実際は erlang モジュールに属しています。 算術演算子や論理演算子、ブール演算子も同様です。erlangモジュールのBIFは他の関数と違って、Erlangを使うときに自動的にインポートされます。 モジュール内に定義された他のすべての関数は、呼び出すときには Module:Function(Arguments) の形でなければいけません。

実際にやってみてください:

1> erlang:element(2, {a,b,c}).
b
2> element(2, {a,b,c}).
b
3> lists:seq(1,4).
[1,2,3,4]
4> seq(1,4).
** exception error: undefined shell command seq/2

ここでリストモジュール内の seq 関数は自動的にはインポートされていません。一方で element は自動的にインポートされています。’undefined shell command’というエラーは、シェルが f() のようなシェルコマンドを探していて、それが見つかりません、と言っているのです。 erlang モジュールには自動的にインポートされない関数もありますが、それらはあまり使われない物です。

論理的に、1つのモジュール内には似た動作をする関数を置くべきです。リストに関する共通の操作は lists モジュールに保存されています。また入出力(ターミナルやファイルへの書き込みなど)に関する関数は io モジュールにあります。 この規則に従わないモジュールは前述の erlang モジュールです。このモジュールには算術や変換、マルチプロセス処理、仮想マシンの設定などに関する関数が含まれています。 組み込み関数以外には共通関数はありません。 erlang のようなモジュールを作るのは避けて、代わりに綺麗な論理的な分離ができるように集中すべきです。

4.1. モジュールの宣言

../_images/declaration1.png

モジュールを書いているときに、2つのものを宣言することができます。関数と属性です。属性はモジュール名や外部に公開する関数、コードの作者名などといったモジュール自身のメタデータです。 こういったメタデータはコンパイラに対してどう処理したらよいか伝えることができるため有用です。 また他の人が使うときにもソースを見なくてもコンパイルされたコードから有用な情報を得ることができるため役に立ちます。

現在世界中にあるErlangのソースコード内には様々な種類のモジュール属性があります。 実際、自分独自の属性を宣言することさえできます。あなたのコード以外にも頻出する事前定義の属性もあります。 すべてのモジュール属性は -Name(Attributes). の形式をとります。 1つだけコンパイルできるようにするために必要な属性があります:

-module(Name).
これはファイルの中で最初の属性(そして最初の宣言)になります。それにはよい理由があります。これは現在のモジュールの名前で、名前はアトムです。この名前は他のモジュールからこのモジュール内の関数を呼ぶときに使います。 関数の呼び出しは M:F(A) の形で行われます。このとき、Mはモジュール名で、Fは関数名、Aは引数です。

いよいよコードを書く時がやって来ました!最初のモジュールはとっても単純で役立たずなものです。テキストエディタを開いて次のように書いて useless.erl という名前で保存しましょう:

-module(useless).

この1行だけの文章も適切なモジュールになっています。本当だよ!もちろん、これは関数なしでは役立たずです。最初にこの「役立たず(’useless’)」モジュールからどんな関数を公開するか決めましょう!これは別の属性を使って行います:

-export([Function1/Arity, Function2/Arity, ..., FunctionN/Arity]).

これはモジュール内のどんな関数が外部に公開されるかを決めるために使います。 関数とそのアリティのリストを引数にとります。関数のアリティというのは引数を何個とるのかという整数値です。 これはとても重要な情報で、あるモジュール内で同じ名前でもアリティが違うだけで異なった関数は別の関数として定義できるからです。 したがって add(X,Y)add(X,Y,Z) は別の関数として処理されて、それぞれ add/2add/3 という形式で書かれます。

Note

公開された関数はモジュールのインタフェースを表します。何が使われる必要があって、何がそうでないのかをきっちりと考えてインタフェースを定義することが重要です。 そうすることで、あなたが書いたモジュールが依存しているコードを壊すことなく、他の「隠れた」ものをいじることができるようになります。

私たちの役立たずモジュールが、初めて役に立つ ‘add’ という名前の関数を公開します。これは2つの引数をとります。次の -export 属性をモジュール宣言のあとに追加します:

-export([add/2]).

これで関数を書きます:

add(A,B) ->
    A + B.

関数の書き方は Name(Args) -> Body という形式をとります。NameはアトムでBodyはカンマで区切られた1つ以上のErlang式です。 関数はピリオドで終わります。Erlangでは’return’キーワードは使わないことに注意してください。 ‘return’は役立たずです!その代わりに関数の最後の論理式が実行されて、その値が呼び出し元に自動的に戻されます。

次の関数を追加しましょう。(なぜなら、どんなチュートリアルにも ‘Hello world’ の例があるからです!もうすでに第4章であってもです!) -export 属性に追加することをお忘れなく。

%% Shows greetings.
%% io:format/1 is the standard function used to output text.
hello() ->
    io:format("Hello, world!~n").

この関数でわかることは、コメントは1行のみで、%記号で始まるということです。( %% はただのスタイルです) hello/0 関数は外部モジュールの関数をどう呼び出すかも示してくれています。この例で io:format/1 はコメントに書かれているように、テキストを出力する標準関数です。

このモジュール最後の関数を追加します。この関数は add/2hello/0 を両方使います:

greet_and_add_two(X) ->
    hello(),
    add(X,2).
../_images/imports1.png

greet_and_add_two/1 を公開関数リストに追加することを忘れずに。 hello/0add/2 はモジュール名を頭に付ける必要ありません。なぜなら同じモジュール内で宣言されているからです。

io:format/1add/2 などの同じモジュール内で宣言された関数と同じような呼び出せたらな、と思ったなら、次のモジュール属性をファイルの先頭に追加することもできます: -import(io, [format/1]). こうすれば、 format("Hello, World!~n") という形で直接呼び出すことができます。 一般的には -import 属性は次のように書きます:

-import(Module, [Function1/Arity, ..., FunctionN/Arity]).

関数をインポートすることはコードを書くときにプログラマがショートカットする以外の効果はありません。 Erlangプログラマはしばしば -import 属性を使うのを嫌います。なぜならコードの可読性が下がるからです。 io:format/2 に場合で言うと、 io_lib:format/2 も存在します。どっちが使われているかはファイルの先頭に行って、どちらのモジュールがインポートされているか確認しなければなりません。 結果として、モジュール名を残すことは良い習慣とされています。通常は、listsモジュールだけインポートしても問題ないように思われます。listsモジュールの関数は他のモジュールの関数とくらべてとてもよく使われるからです。

あなたの useless モジュールはこんな見た目のファイルになっているでしょう:

-module(useless).
-export([add/2, hello/0, greet_and_add_two/1]).

add(A,B) ->
    A + B.

%% Shows greetings.
%% io:format/1 is the standard function used to output text.
hello() ->
    io:format("Hello, world!~n").

greet_and_add_two(X) ->
    hello(),
    add(X,2).

これで “useless” モジュールについてはこれでおしまいです。ファイルを useless.erl という名前で保存します。 ファイル名は -module 属性で定義したモジュール名と同じ名前にして、 ‘.erl’ という拡張子をつけます。 これは標準的なErlangソースコードの拡張子です。

どのようにモジュールをコンパイルして、これらの関数をテストするかをお見せする前に、どのようにマクロを定義して使うかをお見せします。 ErlangのマクロはC言語の ‘#define’ 宣言とよく似ています。C言語では短い関数と定数を定義するために使われます。 マクロは簡単な式で、コードがVM用にコンパイルされる前に置き換えられます。マクロは主にモジュール内にできてしまうマジックバリューを避けるために使われます。 マクロはモジュール属性の形で定義されます。 -define(MACRO, some_value) の形です。そしてモジュール内のどんな関数内でも ?MACRO の形で使えます。 ‘関数’マクロは -define(sub(X,Y), X-Y). の形で定義します。そして ?sub(23,47) と使います。 これはあとでコンパイラに 23-47 に置き換えられます。もっと複雑なマクロを使う人もいますが、基本的な構文は一緒です。

4.2. コードをコンパイルする

Erlangコードは仮想マシンが使うバイトコードにコンパイルされます。コンパイラはどこからでも呼び出せます。コマンドラインでは $ erlc flags file.erl で、シェルやモジュールでは compile:file(FileName) とします。シェルでは c() もできます。

いよいよ私たちの役立たずモジュールをコンパイルして試してみましょう。Erlangシェルを開いて、入力してください:

1> cd("/path/to/where/you/saved/the-module/").
"Path Name to the directory you are in"
ok

デフォルトでは、シェルは起動時にいるディレクトリ内のファイルと標準ライブラリを探します。 cd/1 はErlangシェルだけで定義された関数で、シェルにディレクトリの変更させて、これでファイルを探すのが楽になります。 これが終わったら、次のコードを実行してみましょう:

2> c(useless).
{ok,useless}

これ以外のメッセージが出ていたら、ファイル名が正しく保存されているか確認して下さい。あるいは正しいディレクトリにいるか、あるいはモジュール内に間違いがないか確認して下さい。 無事コードをコンパイルできたら、 useless.beam というファイルが useless.erl の隣に出来ているのに気がつくでしょう。 これがコンパイル済みモジュールです。では早速最初の関数を試してみましょう:

3> useless:add(7,2).
9
4> useless:hello().
Hello, world!
ok
5> useless:greet_and_add_two(-3).
Hello, world!
-1
6> useless:not_a_real_function().
** exception error: undefined function useless:not_a_real_function/0

関数は予想したとおりに動きます: add/2 は数字を加算し、 hello/0 は”Hello, world!” を表示します。 greet_and_add_two/1 は両方をします!もちろん、 hello/0 は文字を出力したあと、なぜ’ok’というアトムを返すのか気になると思います。 これはErlangの関数や式が常に何かを返さなければならないからです。それが他の言語では必要なかったとしてもです。 io:format/1 のように、’ok’は無事終了したことを意味します。

6番目の式は、関数が存在しないので投げられたエラーです。関数をエクスポートし忘れた場合に、このようなエラーメッセージを受け取るでしょう。

Note

何か気になったことがあるかもしれませんが、’.beam’はBogdan/BjörnさんのErlang抽象マシン(Bogdan/Björn’s Erlang Abstract Machine)を表しています。 この名前はVM自身の名前です。Erlangには他の仮想マシンもありますが、それらはあまり使われてないですし、もはや過去の遺物dえす。JAM(Joeさんの抽象マシン)はPrologのWAMや昔のBEAMに触発されたものもありました。それらはErlangをCにコンパイルして、ネイティブコードにするものでした。 ベンチマークでは実際にはちょっと利点がありましたが、このコンセプトは諦められてしまいました。

コンパイルフラグはたくさんあって、モジュールをどのようにコンパイルするかより制御できるようになっています。 Erlangのドキュメントを見れば、すべてのフラグを確認できます。最もよく使われるフラグは以下です:

-debug_info

デバッガ、コードカバレッジ、静的解析ツールのようなErlangツールはモジュールのデバッグ情報を使います。

-{outdir,Dir}

デフォルトでは、Erlangコンパイラは現在いるディレクトリに’beam’ファイルを生成します。 このオプションはどのディレクトリにコンパイル済みファイルを置くか選択できます。

-export_all

このオプションは -export モジュール属性を無視して、定義されているすべての関数を公開します。 これは新しいコードをテストしたり開発したりするときに役に立ちますが、製品においては使うべきではありません。

-{d,Macro} または {d,Macro,Value}

モジュールで使われるマクロを定義します。このときマクロはアトムです。 これはユニットテストを使うときによく使われます。このオプションを使うことで、明示的に必要な場合にのみ公開される、モジュール内のテスト関数を使うかどうかを決めることが出来ます。 デフォルトでは、Valueはタプルの3番目の要素に何も与えられていない場合、’true’になっています。

私たちの useless モジュールをいくつかのフラグをつけてコンパイルするには、次のようにできます:

7> compile:file(useless, [debug_info, export_all]).
{ok,useless}
8> c(useless, [debug_info, export_all]).
{ok,useless}

コンパイルフラグをモジュール属性を使うことでこっそりと定義することもできます。 7番目や8番目の行のような結果を得るためには、次のような一行をモジュールに追加すればいいでしょう:

-compile([debug_info, export_all]).

これで、コンパイルするだけで、手でフラグを与えたときと同様の結果を得ることが出来ます。 これで関数を書いて、コンパイルして、実行して、どういう風に動作するかみることができます!

Note

他のオプションとしてはErlangモジュールをネイティブコードにコンパイルするときに使います。 ネイティブコードにコンパイルすることは、どんなプラットフォームやOSで出来る訳ではないですが、サポートしているところでは、プログラムを普通にコンパイルするよりずっと速く動作させることが出来るようになります。(聞くところでは、約20%速くなります) ネイティブコードにコンパイルするには、 hipe モジュールを使って、次のように呼び出す必要があります: hipe:c(Module,OptionsList). またシェルで同様の結果を得るには、次のように使うことも出来ます: c(Module,[{hipe,o3}]) このようにして生成された .beamファイルはもはや通常の.beamファイルと違って、クロスプラットフォームでないことに気をつけてください。

4.3. モジュールについてもっと詳しく

関数の書き方やそのまま役に立つコードスニペットを学ぶ前に、他にいくつか将来有用な多岐に渡る情報があります。

最初の1つはモジュールのメタデータについてです。この章の最初で、モジュール属性はモジュール自身を記述するメタデータだと言いました。ソースにアクセスせずに、どのようにこのメタデータにアクセスできるようになるのでしょうか? コンパイラがよきにはからってくれます:モジュールをコンパイルするときに、大抵のモジュール属性を拾ってきてくれて、 module_info/0 関数に(他の情報と一緒に)保存してくれます。 useless モジュールのメタデータは次のようにして確認できます:

9> useless:module_info().
[{exports,[{add,2},
{hello,0},
{greet_and_add_two,1},
{module_info,0},
{module_info,1}]},
{imports,[]},
{attributes,[{vsn,[174839656007867314473085021121413256129]}]},
{compile,[{options,[]},
{version,"4.6.2"},
{time,{2009,9,9,22,15,50}},
{source,"/home/ferd/learn-you-some-erlang/useless.erl"}]}]
10> useless:module_info(attributes).
[{vsn,[174839656007867314473085021121413256129]}]

上に挙げたスニペットは追加の関数 module_info/1 を示しています。この関数は特定の情報を取得するために使われます。 公開されている関数や、インポートしている関数(この場合はありません!)、属性(独自のメタデータもここで取得できます)、そしてコンパイルオプションの情報です。 -author("An Erlang Champ"). をあなたのモジュールに追加することを決めたら、 vsn と同じセクションになるでしょう。 製品にする場合はモジュール属性は限られていますが、モジュール属性はあなたの助けになるでしょう。 私はこの本のテストスクリプトでどのユニットテストがよいか関数にアノテーションをするために使っています。 スクリプトはモジュール属性をみて、アノテーションがある関数を探して、それらの関数に対する警告を出します。

Note

vsn はコメントを含めたコードの各バージョンにおいて自動的に生成された一意な値です。 これはコードのホットローディング(実行中にアプリケーションを停止せずにアップグレードする)に使われて、いくつかのツールではリリース処理に使われます。 vsn の値を自分で定義することもできます。モジュールに -vsn(VersionNumber) と追加するだけです。

../_images/circular-dependencies1.png

一般的なモジュールデザインに関してよいアプローチが他にもあります。循環参照を避けることです! モジュールAはモジュールA自身を呼び出しているモジュールBを呼ぶべきではないということです。 このような依存関係は通常はコードの保守を難しくしてしまいます。実際、多くのモジュールに依存しすぎると、それらが循環参照になっていなくてもメンテナンスが難しくなります。 しまいには真夜中に起きたら、マニアックなソフトウェアエンジニアやコンピュータ科学者があなたが書いた恐ろしいコードのせいで、あなたの目玉をノミで繰り抜こうとしているところだった、なんてことになりかねません。

同様の理由で(メンテナンス性と目玉を取られるのを恐れて)、通常は同様の役割を持った関数は近くにまとめるのがよい習慣とされています。 アプリケーションを起動したり停止したり、データベースのレコードを作成したり削除したりするのはそのようなシナリオの例です。

さて、もうひけらかしの知識で「あるべき論」を語るのは十分でしょう。もうちょっとErlangの旅をしてみましょう。