モジュール

モジュールとは

A box with functions written on it

動的プログラミング言語を使う上で、インタラクティブシェルでの作業は非常に重要だと考えられています。あらゆる種類のコードやプログラムをテストするのに便利です。Erlangの基本的なデータ型は、テキストエディタを開いたりファイルを保存したりすることなく使用できました。キーボードを置いて、外でボール遊びをして一日を終えることもできますが、そこで止まってしまうと、ひどいErlangプログラマーになってしまいます。コードはどこかへ保存して使う必要があります!

そこでモジュールが登場します。モジュールとは、単一のファイルに、単一の名前で再グループ化された関数の集まりです。さらに、Erlangのすべての関数はモジュール内で定義する必要があります。もしかしたら気づいていないかもしれませんが、あなたはすでにモジュールを使用しています。前の章で触れた`hd`や`tl`のような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

ここでは、`list`モジュールの`seq`関数は自動的にインポートされませんでしたが、`element`はインポートされました。「undefined shell command」というエラーは、シェルが`f()`のようなシェルコマンドを探しているが見つからないために発生します。`erlang`モジュールには自動的にインポートされない関数もいくつかありますが、それらはあまり頻繁には使用されません。

論理的には、関連する機能を1つのモジュールにまとめるべきです。リストに対する一般的な操作は`lists`モジュールに保持され、端末やファイルへの書き込みなどの入出力を行う関数は`io`モジュールに再グループ化されます。このパターンに従わないモジュールは、前述の`erlang`モジュールだけで、これは数学演算、変換、マルチプロセッシングの処理、仮想マシンの設定の変更などを行う関数が含まれています。これらは組み込み関数であること以外に共通点がありません。`erlang`のようなモジュールを作成することは避け、明確な論理的な分離に焦点を当てるべきです。

モジュール宣言

A scroll with small text on it

モジュールを作成する際には、*関数*と*属性*の2種類を宣言できます。属性とは、モジュール自体の名前、外部から見える関数、コードの作成者など、モジュールを記述するメタデータです。この種のメタデータは、コンパイラにどのように動作すべきかを指示するヒントを与えるため、また、ソースコードを参照することなくコンパイルされたコードから有用な情報を取得できるため、便利です。

世界中のErlangコードで使用されているモジュール属性は多種多様です。実際、独自の属性を自由に宣言することもできます。コードによく登場する定義済み属性がいくつかあります。すべてのモジュール属性は`-Name(Attribute).`という形式に従います。モジュールをコンパイル可能にするために必要な属性は1つだけです。

-module(Name).
これは常にファイルの最初の属性(およびステートメント)です。それも当然で、これは現在のモジュールの名前であり、Nameアトムです。これは、他のモジュールから関数を呼び出すときに使用する名前です。呼び出しは`M:F(A)`の形式で行われ、Mはモジュール名、Fは関数、Aは引数です。

いよいよコーディングの時間です!最初のモジュールは非常にシンプルで役に立たないものにします。テキストエディタを開いて以下を入力し、`useless.erl`という名前で保存してください。

-module(useless).

この1行のテキストは有効なモジュールです。本当に!もちろん、関数がないと役に立ちません。まず、「useless」モジュールからどの関数をエクスポートするかを決めましょう。そのためには、別の属性を使用します。

-export([Function1/Arity, Function2/Arity, ..., FunctionN/Arity]).
これは、モジュールのどの関数を外部から呼び出すことができるかを定義するために使用されます。それぞれのarityを持つ関数のリストを取ります。関数のarityとは、関数に渡すことができる引数の数を表す整数です。これは重要な情報です。なぜなら、モジュール内で定義された異なる関数は、arityが異なる場合にのみ同じ名前を共有できるからです。したがって、関数`add(X,Y)`と`add(X,Y,Z)`は異なるものとみなされ、それぞれ`add/2`と`add/3`と記述されます。

**注:** エクスポートされた関数はモジュールのインターフェースを表します。インターフェースを定義する際には、モジュールを使用するために必要なものだけを公開し、それ以外のものは公開しないことが重要です。そうすることで、モジュールに依存するコードを壊すことなく、実装の他の[隠された]詳細を自由に操作できます。

役に立たないモジュールでは、まず2つの引数を取る`add`という有用な関数をエクスポートします。モジュール宣言の後に、以下の`-export`属性を追加できます。

-export([add/2]).

そして、関数を記述します。

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

関数の構文は`Name(Args) -> Body.`という形式に従います。ここで、Nameはアトムでなければならず、Bodyはカンマで区切られた1つ以上のErlang式です。関数はピリオドで終わります。Erlangは「return」キーワードを使用しないことに注意してください。「return」は不要です!代わりに、関数内で最後に実行される論理式の値が、指定しなくても自動的に呼び出し元に返されます。

`-export`属性に追加することを忘れずに、次の関数(そう、すべてのチュートリアルには「Hello world」の例が必要です!4章目でも!)を追加します。

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

この関数からわかることは、コメントは単一行のみで、`%`記号で始まるということです(`%%`を使用するのは純粋にスタイルの問題です)。`hello/0`関数は、自分のモジュール内から外部モジュールの関数を呼び出す方法も示しています。この場合、コメントに書かれているように、`io:format/1`はテキストを出力するための標準関数です。

最後に、`add/2`と`hello/0`の両方の関数を使用した関数をモジュールに追加します。

greet_and_add_two(X) ->
	hello(),
	add(X,2).
A box being put in another one

`greet_and_add_two/1`をエクスポートされた関数リストに追加することを忘れないでください。`hello/0`と`add/2`の呼び出しには、モジュール名を先頭に付ける必要はありません。なぜなら、それらはモジュール自体で宣言されているからです。

`add/2`やモジュール内で定義された他の関数と同じ方法で`io:format/1`を呼び出せるようにしたい場合は、ファイルの先頭に次のモジュール属性を追加することができます。`-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`モジュールの関数だけです。その関数は、他のほとんどのモジュールの関数よりも使用頻度が高いためです。

あなたの`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`属性で定義されたモジュール名に、Erlangソースの標準拡張子である「.erl」を付けたものにする必要があります。

モジュールをコンパイルして、そのエキサイティングな関数をすべて試してみる前に、マクロを定義して使用する方法を見てみましょう。Erlangのマクロは、Cの「#define」ステートメントと非常によく似ており、主に短い関数や定数を定義するために使用されます。マクロは、VM用にコードがコンパイルされる前に置き換えられるテキストで表される単純な式です。このようなマクロは、モジュール内にマジックナンバーが散らばらないようにするために主に役立ちます。マクロは`-define(MACRO, some_value).`という形式のモジュール属性として定義され、モジュール内で定義された任意の関数内で`?MACRO`として使用されます。「関数」マクロは`-define(sub(X,Y), X-Y).`のように記述し、`?sub(23,47)`のように使用することができます。これは後でコンパイラによって`23-47`に置き換えられます。より複雑なマクロを使用する人もいますが、基本的な構文は同じです。

コードのコンパイル

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シェル専用に定義された関数で、新しいディレクトリに変更するように指示します。これにより、ファイルを探す手間が省けます。Windowsユーザーは、スラッシュを前に付ける必要があることに注意してください。これが完了したら、次のようにします。

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

別のメッセージが表示された場合は、ファイル名が正しいこと、正しいディレクトリにいること、モジュールに間違いがないことを確認してください。コードのコンパイルに成功すると、ディレクトリ内の`useless.erl`の横に`useless.beam`ファイルが追加されていることに気付くでしょう。これがコンパイルされたモジュールです。最初の関数を試してみましょう。

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は、関数が存在しないためにエラーがスローされていることを示しています。関数をエクスポートし忘れた場合、試してみるとこのようなエラーメッセージが表示されます。

**注:** もし疑問に思っているなら、「.beam」は*Bogdan/Björn's Erlang Abstract Machine*の略で、VMそのものです。Erlang用の他の仮想マシンも存在しますが、それらは実際にはもう使用されておらず、歴史的なものです。JAM (Joe's Abstract Machine、PrologのWAMにインスパイアされたもの) と古いBEAM (ErlangをCに、そしてネイティブコードにコンパイルしようとしたもの) です。ベンチマークでは、この方法にはほとんど利点がないことが示され、この概念は放棄されました。

モジュールのコンパイル方法をより詳細に制御するためのコンパイルフラグが多数存在します。すべてのフラグのリストは、Erlangのドキュメントに記載されています。最も一般的なフラグは以下のとおりです。

-debug_info
デバッガ、コードカバレッジ、静的解析ツールなどのErlangツールは、モジュールのデバッグ情報を使用して動作します。
-{outdir,Dir}
デフォルトでは、Erlangコンパイラは現在のディレクトリに「beam」ファイルを作成します。このフラグを使用すると、コンパイルされたファイルの出力先を選択できます。
‐export_all
-export モジュール属性を無視し、代わりに定義されているすべての関数をエクスポートします。これは主に新しいコードのテストと開発に役立ちますが、本番環境では使用しないでください。
-{d,Macro} または {d,Macro,Value}
モジュール内で使用するマクロを定義します。ここで、Macro はアトムです。これは、単体テストを扱う際に、モジュールが明示的に必要な場合にのみテスト関数が作成およびエクスポートされるようにするために、より頻繁に使用されます。デフォルトでは、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]).

コンパイルするだけで、手動でフラグを渡した場合と同じ結果が得られます。これで関数を記述し、コンパイルして実行できるようになったので、どこまでできるか見てみましょう!

注: もう1つの選択肢は、Erlangモジュールをネイティブコードにコンパイルすることです。ネイティブコードのコンパイルは、すべてのプラットフォームとOSで利用できるわけではありませんが、サポートされているプラットフォームとOSでは、プログラムの速度を向上させることができます(事例証拠に基づくと、約20%高速です)。ネイティブコードにコンパイルするには、hipe モジュールを使用し、hipe:c(Module,OptionsList) のように呼び出す必要があります。シェルで c(Module,[native]) を使用して同様の結果を得ることもできます。生成される.beamファイルには、ネイティブコードと非ネイティブコードの両方が含まれ、ネイティブ部分はプラットフォーム間で移植できないことに注意してください。

モジュールについて

関数の記述とほとんど役に立たないコードスニペットについてさらに学ぶ前に、将来役立つ可能性のあるその他の情報をいくつか説明します。

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 と同じセクションに配置されます。本番環境に関するモジュール属性の使用には制限がありますが、自分自身を助けるためのちょっとしたトリックを実行するときに役立ちます。この本のテストスクリプトで使用して、単体テストを改善できる関数を注釈付けしています。スクリプトはモジュール属性を調べ、注釈付きの関数を見つけて、それらに関する警告を表示します。

注: vsn は、コメントを除く、コードの各バージョンを区別する自動的に生成される一意の値です。コードのホットローディング(アプリケーションを停止せずに実行中にアップグレードする)およびリリース処理に関連する一部のツールで使用されます。必要に応じて、vsn 値を自分で指定することもできます。モジュールに -vsn(VersionNumber) を追加するだけです。

A small graph with three nodes: Mom, Dad and You. Mom and Dad are parents of You, and You is brother of Dad. Text under: 'If circular dependencies are digusting in real life, maybe they should be disgusting in your programs too'

もう1つのポイントは、一般的なモジュール設計に関するものです。循環依存を避けてください!モジュール A は、モジュール A も呼び出すモジュール B を呼び出すべきではありません。このような依存関係は、通常、コードの保守を困難にします。実際、循環依存関係にない場合でも、あまりにも多くのモジュールに依存すると、保守が難しくなる可能性があります。あなたが望む最後のことは、あなたが書いたひどいコードのために、狂ったソフトウェアエンジニアまたはコンピューター科学者があなたの目をえぐり取ろうとしているのを見つけるためだけに、真夜中に目を覚ますことです。

同様の理由(保守と目の恐怖)から、同様の役割を持つ関数を近くにまとめることは、一般的に良い習慣と考えられています。アプリケーションの開始と停止、または一部のデータベースでのレコードの作成と削除は、このようなシナリオの例です。

さて、お説教はこれで十分です。Erlangをもう少し詳しく調べてみませんか?