-
http://www.erlang.se/doc/programming_rules.shtml を読むがてら日本語訳を作成する。
- ドメインがなくなってしまったのでコピー記事を見つけた:https://docs.jj1bdx.tokyo/Erlang_Programming_Rules.html
- 短くまとまった記事:http://teahut.sakura.ne.jp/b/2008-06-25-2.html
- 悪文が多いです。ぜひ編集リクエストお願いします。
以下訳文
Author: EPK/NP Klas Eriksson, EUA/SU M Williams, JArmstrong
Document: EPK/NP 95:035
Erlangを使用したプログラム開発 - プログラミングルールと規約
概要
Erlangを使用したシステムの書き方のアドバイスやプログラミングのルールを解説します。
注意: この文書は予備的なものであり、完全なものではありません。
EBCの「Base System」を使用するための要件は、ここでは説明されません。しかし、「Base System」を使用する場合には、非常に初期の設計段階に従わなければなりません。これらの要件は 1/10268-AND 10406 Uen "MAP - Start and Error Recovery" で説明されます。
1 目的
この文書では、Erlangを使用してソフトウェアシステムの仕様を定めたりプログラミングする際に考慮すべきいくつかの側面を示します。これはErlangの使用に関わらない一般的な仕様や設計活動の完全に説明するものではありません。
2 構成とErlang用語
Erlangシステムは**モジュール(Modules)に分割されます。モジュールは関数(functions)と属性(attributes)から構成されます。関数はモジュールの内部から見えるものかエクスポート(exported)**されるもののどちらかです。エクスポートされた関数は、他のモジュール内の他の関数からも呼び出すことができます。属性は-
で始まり、モジュールの始めに配置されます。
Erlangを使用して設計されたシステムの処理は**プロセス(prosesses)**によって行われます。プロセスは、多くのモジュールの関数を使用することのできるジョブです。プロセスは、**メッセージ送信(sending message)によって相互に通信します。プロセスは、送信されたメッセージを受信(receive)**し、受信準備ができているメッセージを決定できます。他のメッセージは、受信プロセスが受信準備を完了するまでキューイングされます。
プロセスは、**リンク(link)を設定することにより、既に存在するもう一つのプロセスを監視できます。プロセスは終了すると、自動的に終了シグナル(exit signal)をリンクしているプロセスに送信します。終了シグナルを受け取ったプロセスは、デフォルトで終了し、さらに、リンクされたプロセスにシグナルを広めるように動作します。プロセスは終了トラップ(trapping exits)**によってデフォルトの動作を変更でき、これによりすべてのプロセスへの終了シグナルは、メッセージへと変化させられます。
純粋な関数とは、関数呼び出しの文脈に関わらず同じ引数が与えられれば同じ値を返す関数のことです。これは、通常数学的な関数に期待されるものです。純粋でない関数は、副作用を持つと表現されます。
典型的には、関数 a)、メッセージ送信
b)、メッセージ受信 c)、exit呼び出し d)、プロセス環境もしくは動作モードを変更するいずれかのBIFを呼び出す場合に副作用は生じます。(例: get/1, put/2, erase/1, process_flag/2, その他)
警告: この文書は、良くないコード例が含まれます。
3 ソフトウェアエンジニアリング原則
3.1 なるべく少数の関数のみモジュールからエクスポートする
モジュールは、Erlangの基本的なコード構造の実体です。一つのモジュールは、たくさんの関数を含むことができ、モジュールのエクスポートリストに含まれる関数のみがモジュールの外側から呼び出されることができます。
エクスポートされた関数/エクスポートされていない関数の比率が低いモジュールは、モジュールのユーザーがエクスポートされた関数の機能だけを理解すればよいという点で望ましいです。
加えて、モジュール内のコードの作者やメンテナーは、外部インターフェイスを変更しなければ、適切な方法でモジュールの内部構造を変更できます。
3.2 モジュール間の依存性を減らしてみる
多数の異なるモジュールの関数を呼び出すモジュールは、少数の異なるモジュールの関数を呼び出すモジュールに比べてメンテナンスが難しいです。
モジュールインターフェイスを変更する度に、このモジュールが呼び出されるすべての箇所をチェックする必要があります。モジュール間の依存性の減少はこれらのモジュールをメンテナンスする問題をシンプルにします。
特定のモジュールから呼び出される異なるモジュールの数を減らすことによってシステム構造のシンプル化が可能です。
モジュール間の呼び出し依存形態がサイクルグラフでなく、ツリーが望ましいことにも気をつけてください。例:
以下はダメ
3.3 汎用的に使用されるコードはライブラリに入れる
汎用的に使用されるコードはライブラリに配置されるべきです。ライブラリは関連する関数のコレクションにすべきです。ライブラリに同じ型の関数が含まれることを保証するには多大な労力が必要です。したがって、リストを操作する関数のみを含むlists
のようなライブラリは良い選択となり、一方、リスト操作と数学的な関数の組み合わせを含むlists_and_maths
のような関数は非常に悪い選択です。
理想のライブラリは副作用を持ちません。副作用のある関数を伴うライブラリは再利用性を制限します。
3.4 「トリッキー」なコードや「ダーティ」なコードは別のモジュールに隔離する
多くの場合、問題はクリーンなコードとダーティなコードを混在させて使用することで解決されます。クリーンなコードとダーティーなコードを別のモジュールに分離してください。
ダーティなコードはダーティーなことを行うコードです。例:
- プロセス辞書を使用する。
-
erlang:process_info/1
を不自然な目的で使用する。 - (必要に迫られ)やってはならないことをやる。
クリーンなコードの量を最大化し、ダーティーなコードの量を最小化することに集中してください。ダーティなコードを隔離し、ダーティなコードに関連する全ての副作用と問題をドキュメント化してください。
3.5 呼び出し側が関数の結果をどうするかについて仮定を作らない
関数が呼ばれた理由や、関数の呼び出し側が結果に対して何をしたいかについて仮定を作らないでください。
例えば、不正かもしれない特定の引数でルーチンを呼び出した場合を仮定します。ルーチンの実装者は、引数が不正な時に関数の呼び出し側が何を望むことについて、いかなる仮定もすべきではありません。
したがって、次のようなコードは書くべきではありません。
do_something(Args) ->
case check_args(Args) of
ok ->
{ok, do_it(Args)};
{error, What} ->
String = format_the_error(What),
io:format("* error:~s\n", [String]), %% Don't do this
error
end.
代わりに次のように書いてください。
do_something(Args) ->
case check_args(Args) of
ok ->
{ok, do_it(Args)};
{error, What} ->
{error, What}
end.
前者ではエラーシグナルは常に標準出力に表示され、後者ではエラーディスクリプタがアプリケーションに返されます。アプリケーションは、このエラーディスクリプタを何をするか決定することができます。
error_report/1
の呼び出しによりアプリケーションは、エラーディスクリプタを表示可能な文字列に変換し、それを表示します。しかしこれは望ましい動作ではないかもしれません。どのような場合でも、結果をどう処理するかの決定は呼び出し元に任されます。
3.6 コードや動作の一般的なパターンを抽象化する
コードの2箇所以上に同じパターンのコードがある場合、コードを2つの異なる場所に配置する代わりに、共通の関数でこれを分離し、代わりにこの関数を呼び出してください。コピーされたコードはメンテナンスに多大な労力が求められます。
コード内の2箇所以上で似たようなパターン(ほとんど同一の)コードを見つけた場合、問題を少し変更して異なるケースを同一にできないをチェックし、2箇所の違いを記述するために少量の追加コードを書くことができないかチェックするために時間を取る価値があります。
コピぺプログラミングはやめて、関数を使ってください!
3.7 トップダウン
(詳細から始める)ボトムアップでなく、トップダウンでプログラムを書いてください。トップダウンは、プリミティブな関数を定義で終わり、実装の詳細に逐次アプローチする良い方法です。上位レベルのコードの設計時点では表現がわからないため、コードは表現に依存しないものになります。
3.8 コードを最適化しない
最初の段階でコードを最適化しないでください。まず正しく作り、そのあと(もし必要なら)高速化をしてください。(コードは正しく保ち続けます。)
3.9 驚き最小の原則を活用する
システムは、なるべく「驚き最小」の方法で反応すべきです。すなわち、ユーザーは何が起こるか予測でき、何かした時に結果に驚かないようにすべきです。
これは一貫性に関係があり、別々のモジュールが同じ規則のもとで処理を行うような一貫したシステムは、それぞれのモジュールが異なる規則のもとで処理を行うシステムよりずっと理解しやすいです。
関数の実行で驚きが生じた場合、関数が間違った問題を解決しているか、関数の名前が悪いかのどちらかです。
3.10 副作用の排除を試みる
Erlangには副作用のあるプリミティブがいくつかあります。このプリミティブを利用する関数は、環境に恒久的な変更を生じさせ、そのようなルーチンを呼び出す前にプロセスの正確な状態を知る必要があるため、簡単に再利用することはできません。
可能な限り副作用のないコードを書いてください。
純粋な関数の数を最大化してください。
副作用のある関数をまとめて、すべての副作用を明確にドキュメント化してください。
少しの注意を払えば、ほとんどのコードは副作用なしで書くことができます。これは保守、テスト、理解を非常に簡単にします。
3.11 モジュールの外にプライベートデータ構造の"漏れ"出しを許さない
これは、簡単な例で説明するのが一番です。キューを実装するためにqueue
と呼ばれるシンプルなモジュールを定義します。
-module(queue).
-export([add/2, fetch/1]).
add(Item, Q) ->
lists:append(Q, [Item]).
fetch([H|T]) ->
{ok, H, T};
fetch([]) ->
empty.
ここではリストとしてキューを実装しますが、残念ながらこれを使うユーザーはキューがリストとして表現されることを知らなければならなりません。このコードを使う典型的なプログラムは以下のようなコードの断片を含んでいるでしょう。
NewQ = [], % Don't do this
Queue1 = queue:add(joe, NewQ),
Queue2 = queue:add(mike, Queue1), ....
これは以下の理由でマズいです。 a) ユーザーは、キューがリストで表現されることを知る必要があり、 b) 実装者は、キューの内部表現を変更することができない(後にモジュールのより良いバージョンを提供するために変更するかもしれない)。
より良いのは:
-module(queue).
-export([new/0, add/2, fetch/1]).
new() ->
[].
add(Item, Q) ->
lists:append(Q, [Item]).
fetch([H|T]) ->
{ok, H, T};
fetch([]) ->
empty.
これによってこのように書けます。
NewQ = queue:new(),
Queue1 = queue:add(joe, NewQ),
Queue2 = queue:add(mike, Queue1), ...
これははるかに優れており、この問題を修正します。ユーザーがキューの長さを知る必要がある場合、このように書きたくなるでしょう。
Len = length(Queue) % Don't do this
なぜならユーザーはキューがリストで表現されることを知っているからです。繰り返しになりますが、これは悪いプログラミングプラクティスであり、コードのメンテナンスと理解を非常に難しくします。もしユーザーがキューの長さを知る必要がある場合、length関数はモジュールに追加されなければなりません。したがって:
-module(queue).
-export([new/0, add/2, fetch/1, len/1]).
new() -> [].
add(Item, Q) ->
lists:append(Q, [Item]).
fetch([H|T]) ->
{ok, H, T};
fetch([]) ->
empty.
len(Q) ->
length(Q).
ユーザーは、代わりに queue:len(Queue)
を呼べます。
ここでは、キューのすべての詳細を「抽象化」したと言います。(キューは実際に抽象データ型と呼ばれます。)
なぜこんな面倒なことをしないといけないんでしょうか?内部の実装詳細の抽象化のプラクティスは、修正するモジュール内の関数の呼び出し側のコードの変更を伴わずに、実装の変更を可能にします。そこで、例えば、キューのより良い実装は以下の通りです。
-module(queue).
-export([new/0, add/2, fetch/1, len/1]).
new() ->
{[],[]}.
add(Item, {X,Y}) -> % Faster addition of elements
{[Item|X], Y}.
fetch({X, [H|T]}) ->
{ok, H, {X,T}};
fetch({[], []) ->
empty;
fetch({X, []) ->
% Perform this heavy computation only sometimes.
fetch({[],lists:reverse(X)}).
len({X,Y}) ->
length(X) + length(Y).
3.12 可能な限りコードを決定論的にする
決定論的プログラムでは、何回プログラムを実行したかに関わらず、常に同じ規則のもとで実行されます。非決定論的プログラムは、それぞれの実行ごとに異なった結果を提供するかもしれません。デバッグ目的で、可能な限りものごとを決定論的にするのは良いアイデアです。これはエラーの再現性を助けるでしょう。
例えば、一つのプロセスが5つの並列プロセスを起動して、それらが正しく起動されたことをチェックする必要があると仮定し、さらに、これら5つのプロセスが起動される順序は重要でないと仮定します。
その時、5つすべてが並列で起動し、すべてが正確に起動してからチェックするか、もっと良い方法として、1つを起動し、それぞれが正確に起動したことをしてから次の軌道に映るかのどちらかを選択することができます。
3.13 防御的にプログラムしない
防御的プログラムは、プログラマがプログラミングするシステムの一部への入力データを「信頼」しないプログラムです。一般に、関数の入力データの正しさはテストしないでください。システム内のコードのほとんどは、関数の入力データが正しいと仮定して書かれるべきです。コードのごく一部だけがが実際にデータのチェックを行います。これは通常、データがシステムに初めて「投入」された時のみ行われ、システムへの投入データは一度チェックされた以降、正しいとして見なされます。
例:
%% Args: Option is all|normal
get_server_usage_info(Option, AsciiPid) ->
Pid = list_to_pid(AsciiPid),
case Option of
all -> get_all_info(Pid);
normal -> get_normal_info(Pid)
end.
Option
が normal
でも all
でもない場合、関数はクラッシュしますが、そうすべきです。呼び出し側は、正しい入力を提供する必要があります。
3.14 デバイスドライバでハードウェアインターフェースを分離する
ハードウェアは、デバイスドライバを通してシステムから分離されるべきです。デバイスドライバは、ハードウェアがErlangプロセスかのように見えるようなハードウェアインタフェースを実装すべきです。ハードウェアは、通常のErlangプロセスのように見え、動作すべきです。ハードウェアは通常のErlangメッセージを送受信するように見え、エラー発生時には慣例通りに対応すべきです。
3.15 関数内でしたことは同じ関数内で片付ける
ファイルをオープンし、何かをして、あとでクローズするプログラムがあるとします。このようなコードになるはずです。
do_something_with(File) ->
case file:open(File, read) of,
{ok, Stream} ->
doit(Stream),
file:close(Stream) % The correct solution
Error -> Error
end.
同一ルーチンでのファイルのオープン(file:open
)とクローズ(file:close
)の対称性に注目してください。以下のソリューションは非常に追いづらく、ファイルが閉じられたことが明確ではありません。このようにプログラムしないでください。
do_something_with(File) ->
case file:open(File, read) of,
{ok, Stream} ->
doit(Stream)
Error -> Error
end.
doit(Stream) ->
....,
func234(...,Stream,...).
...
func234(..., Stream, ...) ->
...,
file:close(Stream) %% Don't do this
4 エラーハンドリング
4.1 エラーハンドリングと通常ケースのコードを分離する
例外処理を表現するコードで「通常ケース」のコードを取り散らかさらないでください。可能な限り通常ケースだけでプログラムすべきです。もし通常ケースのコードが失敗した場合、プロセスは、至急エラーとクラッシュを伝えるべきです。エラーを修正せず、そのまま続けてください。エラーは異なったプロセスで処理されるべきです。 (「各プロセスは一つだけの「役割」を持つべき」参照)
4.2 エラーカーネルを識別する
システム設計の基本要素の一つは、どの部分を正しくするべきでどの部分を不正とするべきか識別することです。
従来のオペレーティングシステムの設計では、システムのカーネルは、正しいとする一方、全てのユーザーアプリケーションプログラムは必ずしも正しい必要はありません。ユーザーアプリケーションプログラムが失敗した場合、失敗の起きた箇所がアプリケーションに関するだけのものであれば、システム全体の完全性に影響を与えるべきではありません。
システムの最初の設計段階で必ず、正しくあるシステムの部分を識別しなければなりません。そのような箇所をエラーカーネルと呼びます。多くの場合、エラーカーネルは、ハードウェアの状態を格納するある種のリアルタイムメモリ常駐データベースを所持します。
5 プロセスとサーバーとメッセージ
5.1 一つのモジュールでプロセスを実装する
シングルプロセスで実装されたコードは、一つのモジュールを保持するべきです。プロセスはどんなライブラリルーチンの関数を呼ぶこともできますが、プロセスの「トップループ」は一つのモジュールで保持するべきです。プロセスのトップループのコードは複数プロセスに分割すべきでありません。この分割は制御フローの理解を非常に難しくします。これは汎用サーバーライブラリを利用すべきでないという意味ではなく、制御フローの構造化を支援するためのものです。
逆に、1つのモジュールに実装するプロセスの種類は1つだけにするべきです。複数の異なるプロセスを保持するモジュールは非常に理解が難しいです。個々のプロセスに対するコードは別々のモジュールに分割するべきです。
5.2 システム構築のためにプロセスを使用する
プロセスは基本的なシステム構成要素です。しかし、関数呼び出しの代わりにプロセスとメッセージパッシングを利用してはいけません。
5.3 登録プロセス
登録プロセスはモジュール名と同名で登録されるべきです。これによってプロセスのためのコードを見つけやすくなります。
登録プロセスだけが、長期間生き続けます。
5.4 システム内の実際の同時アクティビティごとに1つの並列プロセスを割り当てる
逐次プロセス、並列プロセスのどちらを使って実装するかを決めるとには、問題の本質的な構造によって示される構造を使うべきです。主なルールは以下の通りです。
「一つの並列プロセスを使用して、現実世界で本当に平行したアクティビティをモデル化する」
並列プロセス数と現実世界の本当に平行したアクティビティ数の1対1のマッピングがあれば、プログラムの理解は簡単になるでしょう。
5.5 各プロセスは一つだけの「役割」を持つべき
プロセスはシステム内で異なる役割を持ちます。例えばクライアント-サーバーモデルです。
可能な限りプロセスは一つだけの役割を持つべきです。すなわち、一つのプロセスはクライアントもしくはサーバーになれますが、これらの役割を組み合わせるべきではありません。
プロセスがもつ可能性のある他の役割は次の通りです。
スーパーバイザー: 他のプロセスを監視し、プロセスが失敗した場合には再起動する。
ワーカー: 通常のワークプロセス。(エラーを伴うことができる)
トラステッドワーカー: エラーが許可されていない。
5.6 サーバーのための汎用関数と可能な場所でプロトコルハンドラーを使用する
多くの環境で標準ライブラリ内のgeneric
サーバー実装のような汎用サーバープログラムを使用することは良い考えです。少数の汎用サーバーを一貫して使用するとシステム全体の構造が大幅にシンプル化されます。
システム内のプロトコルハンドリングソフトウェアのほとんどで同様です。
5.7 タグメッセージ
全てのメッセージはタグ付きであるべきです。これはreceive文の順序が重要で亡くなり、新しいメッセージの実装を簡単にします。
このようにプログラムしてはいけません。
loop(State) ->
receive
...
{Mod, Funcs, Args} -> % Don't do this
apply(Mod, Funcs, Args},
loop(State);
...
end.
{Mod, Func, Args}
メッセージの下に、新しいメッセージ{get_status_info, From, Option}
を配置するとコンフリクトが生じるでしょう。
メッセージが同期的な場合、返送メッセージは、返されるメッセージを記述する新しいアトムをタグ付けすべきです。例:到着メッセージがget_status_info
でタグ付けされる場合、返されるメッセージはstatus_info
でタグ付けされます。異なるタグを選択する理由の一つはデバッグを容易にするためです。
こちらが良いソリューションです。
loop(State) ->
receive
...
{execute, Mod, Funcs, Args} -> % Use a tagged message.
apply(Mod, Funcs, Args},
loop(State);
{get_status_info, From, Option} ->
From ! {status_info, get_status_info(Option, State)},
loop(State);
...
end.
5.8 未知のメッセージをフラッシュする
すべてのサーバーは、recive
文の中で最低でも一つのOther
の選択肢を持つべきです。これはメッセージキューが溢れるのを避けるためです。例:
main_loop() ->
receive
{msg1, Msg1} ->
...,
main_loop();
{msg2, Msg2} ->
...,
main_loop();
Other -> % Flushes the message queue.
error_logger:error_msg(
"Error: Process ~w got unknown msg ~w~n.",
[self(), Other]),
main_loop()
end.
5.9 末尾再帰サーバーを書く
すべてのサーバーは末尾再帰でなければならず、さもなければシステムがメモリを使い切るまで、サーバーはメモリを消費し続けます。
このようにプログラムしてはいけません。
loop() ->
receive
{msg1, Msg1} ->
...,
loop();
stop ->
true;
Other ->
error_logger:log({error, {process_got_other, self(), Other}})
loop()
end,
io:format("Server going down"). % Don't do this!
% This is NOT tail-recursive
これが正しいソリューションです。
loop() ->
receive
{msg1, Msg1} ->
...,
loop();
stop ->
io:format("Server going down");
Other ->
error_logger:log({error, {process_got_other, self(), Other}}),
loop()
end. % This is tail-recursive
ある種のサーバーライブラリを使用する場合、例えばgeneric
などの場合、自動的にこのミスを回避しています。
5.10 インターフェイス関数
可能なときにはインターフェイスのための関数を使い、直接メッセージを送信することを避けてください。インターフェイス関数にメッセージパッシングをカプセル化します。これをできない場合もあります。
メッセージプロトコルは、内部情報であり他のモジュールから隠すべきです。
インターフェイス関数の例:
-module(fileserver).
-export([start/0, stop/0, open_file/1, ...]).
open_file(FileName) ->
fileserver ! {open_file_request, FileName},
receive
{open_file_response, Result} -> Result
end.
...<code>...
5.11 タイムアウト
receive
文ではafter
を使用することを注意がけてください。メッセージが遅れて届いたケースの処理を保証してください。(「未知のメッセージはフラッシュする」参照)
5.12 トラップexit
可能な限り少ないプロセスがexitシグナルをトラップするべきです。プロセスはexitをトラップするかしないかのどちらかでなければなりません。トラップexitを「トグル」するようなプロセスは、たいていの場合で非常に悪いプラクティスです。
6 様々なErlang固有の規約
6.1 原則的なデータ構造としてレコードを使用する
原則的なデータ構造としてレコードを使用してください。レコードはタグ付きタプルでErlang 4.3以降(EPK/NP 95:034参照)のバージョンで導入されました。Cのstruct
やPascalのrecord
に似ています。
レコードがいくつかのモジュールで使われる場合、その定義はモジュールからインクルードされるヘッダーファイル(拡張子: .hrl)に配置されるべきです。もしレコードが一つのモジュール内のみから使用される場合、レコードの定義はモジュールが定義されるファイルの始めにあるべきです。
Erlangのレコード機能はモジュール間のデータ構造の一貫性を保証することができ、また、モジュール間でデータ構造を渡すときにインターフェイス関数で使用されるべきです。
6.2 セレクタとコンストラクタを使用する
レコードのインスタンスを管理するためにレコード機能によって提供されるセレクタとコンストラクタを使用してください。レコードはタプルであると、明示的に仮定してパターンマッチを使用してはいけません。例:
demo() ->
P = #person{name = "Joe", age = 29},
#person{name = Name1} = P,% Use matching, or...
Name2 = P#person.name. % use the selector like this.
このようにプログラムしないでください。
demo() ->
P = #person{name = "Joe", age = 29},
{person, Name, _Age, _Phone, _Misc} = P. % Don't do this
6.3 タグ付き返り値を使用する
タグ付き返り値を使用してください。
このようにプログラムしないでください。
keysearch(Key, [{Key, Value}|_Tail]) ->
Value; %% Don't return untagged values!
keysearch(Key, [{_WrongKey, _WrongValue} | Tail]) ->
keysearch(Key, Tail);
keysearch(Key, []) ->
false.
{Key, Value}
はfalseを含むことができません。こちらが正しいソリューションです。
keysearch(Key, [{Key, Value}|_Tail]) ->
{value, Value}; %% Correct. Return a tagged value.
keysearch(Key, [{_WrongKey, _WrongValue} | Tail]) ->
keysearch(Key, Tail);
keysearch(Key, []) ->
false.
6.4 細心の注意を払ってキャッチアンドスローを使用する
何をしているか正確に知らない限り、catch
とthrow
を使わないでください!catch
とthrow
は可能な限り使用しないでください。
catch
とthrow
は、プログラムが複雑で信頼性の低い入力(自身の信頼性の高いプログラムからでなく、外界からの入力)を処理する際に、コードの奥深くにある多くの場所でエラーが発生する可能性がある場所で便利です。一つの例としてコンパイラがあります。
6.5 細心の注意を払ってプロセス辞書を使用する
何をしているか正確に知らない限り、get
やput
などを使用してはいけません。get
とput
などは可能な限り使用しないでください。
プロセス辞書に使用される関数は新しい引数の導入により書き直されます。
例:
このようにプログラムしないでください。
tokenize([H|T]) ->
...;
tokenize([]) ->
case get_characters_from_device(get(device)) of % Don't use get/1!
eof -> [];
{value, Chars} ->
tokenize(Chars)
end.
正しいソリューション:
tokenize(_Device, [H|T]) ->
...;
tokenize(Device, []) ->
case get_characters_from_device(Device) of % This is better
eof -> [];
{value, Chars} ->
tokenize(Device, Chars)
end.
get
とput
の使用すると、関数が同じ入力で呼び出されたときに異なる動作をするようになります。これは非決定的であることから、コードの可読性が下がります。get
とput
を使用する関数は、引数だけでなくプロセス辞書の関数でもあるので、デバッグがより複雑になります。Erlangのランタイムエラーの多く(例えばbad_match
)は関数への引数は含まれますが、プロセス辞書は含まれません。
6.6 インポートを使用しない
-import
を使用しないでください。どのモジュールが関数を定義しているか直接確かめることができないため、-import
の使用は可読性が下がります。モジュールの依存関係を見つけるためにexref
(クロスリファレンスツール)を使用してください。
6.7 エクスポート関数
関数がエクスポートされる理由を区別してください。(例えば)関数は以下の理由でエクスポートされます。
- モジュールへのユーザーインターフェイス
- 他のモジュールへのインターフェイス関数
-
apply
,spawn
, ... から呼び出される。ただし、そのモジュール内のみ。
-export
を異なるグループ分けをし、それごとに応じたコメントをつけてください。例:
%% user interface
-export([help/0, start/0, stop/0, info/1]).
%% intermodule exports
-export([make_pid/1, make_pid/3]).
-export([process_abbrevs/0, print_info/5]).
%% exports for use within module only
-export([init/1, info_log_impl/1]).
7 固有字句と文体規約
7.1 深くネストされたコードを書かない
ネストされたコードは、case/if/receive
文の中に他のcase/if/receive
文を含んでいます。これは深くネストされたコードの悪いプログラミングスタイルです。コードはページの右側へと流れていきすぐに読みづらくなる傾向があります。コードの大半で最大2段階のインデント制限を試みてください。これは、短い関数にコードを分割することで実現できます。
7.2 巨大なモジュールを書かない
モジュールは、ソースコードに400行以上含まないべきです。一つの大きなモジュールより複数の小さなモジュールの方が優れています。
7.3 長大な関数を書かない
15~20行を超えるコードで関数を書かないでください。大きな関数は複数の小さな関数に分割してください。長い行を書いて問題を解決しないでください。
7.4 長大な行を書かない
長大な行を書かないでください。行は80文字以上を含まないべきです。(例えば、A4ページに収まります。)
Erlang4.3以降ではstring定数は自動的に連結されます。例えば、
io:format("Name: ~s, Age: ~w, Phone: ~w ~n"
"Dictionary: ~w.~n", [Name, Age, Phone, Dict]
7.5 変数名
意味のある変数名を選んでください。これは非常に難しいです。
変数名がいくつかの単語から構成される場合、"_"、もしくは、大文字を使って単語を分割してください。例: My_variable
もしくは MyVariable
無関心な変数に"_"を使用するのは避け、代わりに"_"で始まる変数を使用してください。例: _Name
。変数の値があとの段階で必要な場合、先頭のアンダースコアを消すだけです。アンダースコアの置き換えは問題が起きず、コードが読みやすくなるでしょう。
7.6 関数名
関数名は、関数が行うことと正確に一致していなければなりません。関数名が示す引数の種類を返す必要があります。読者を驚かせるべきではありません。慣習的な関数には慣習的な名前を使ってください。(start
, stop
, init
, main_loop
異なるモジューの同じ問題を解決する関数は同じ名前を持つべきです。
例:Module:module_info().
悪い関数名は、最も一般的なプログラミングエラーの1つです。名前の適切な選択は非常に難しいです。
異なる関数をたくさん書く場合、いくつかの命名規約はとても役立ちます。例えば、名前の接頭辞"is_"は、trueかfalseのアトムを返す関数であることを意味するのに使用されます。
is_...() -> true | false
check_...() -> {ok, ...} | {error, ...}
7.7 モジュール名
Erlangはフラットなモジュール構造をしています。(すなわち、モジュール内のモジュールは存在しません)ただし、階層的なモジュール構造の効果を試したい場合がよくあります。これは同じモジュール接頭辞を持つ関連モジュールのセットで行うことができます。
例えば、ISDNハンドラーが5つの異なる関連モジュールを使用して実装される場合、これらのモジュールはこのような名前を与えるべきです。
isdn_init
isdn_partb
isdn_...
7.8 一貫した方法のもとでプログラムをフォーマットする
一貫したプログラミングスタイルは、様々な人がコードを理解するのに役立ちます。異なる人々がインデントやスペースについて異なるスタイルを持っています。
例えば、あなたはタプルの要素の間をカンマだけで書きたくなるかもしれません。
{12,23,45}
他の人は、カンマに続いてスペースを入れたいかもしれません。
{12, 23, 45}
一度スタイルを採用した場合、それに固執するべきです。
大きなプロジェクト内では、全ての部分で同じスタイルが使用されるべきです。
8 コードのドキュメント化
8.1 属性コード
モジュールヘッダーのすべてのコードを常に正しく属性付けする必要があります。モジュールに貢献する全てのアイデアがどこから来たかを示し、他のコードに由来するコードの場合、コードはどこから得たか、とその作者を示します。
コードを盗んではいけません。コードを盗むこととは、他のモジュールからコードを取得し、編集し、誰がオリジナルを書いたか示すのを忘れることです。
便利な属性の例です。
-revision('Revision: 1.14 ').
-created('Date: 1995/01/01 11:21:11 ').
-created_by('eklas@erlang').
-modified('Date: 1995/01/05 13:04:07 ').
-modified_by('mbj@erlang').
8.2 コード内で仕様への参照を提供する
コードの理解に関連する任意のドキュメントへのクロスリファレンスをコード内で提供してください。例えば、コードがいくつかのコミュニケーションプロトコルやハードウェアインターフェイスを実装している場合、コードの記述に使用されたドキュメントとそのページ番号への正確な参照を与えてください。
8.3 すべてのエラーをドキュメント化する
すべてのエラーは、それが意味するものの英語の説明とともに別の文書にリストされるべきです。(10.4 エラーメッセージ)
エラーとは、システムによって検出されたエラーを意味します。
プログラムが論理エラーを検出した時点で、エラーロガーを呼び出します。
error_logger:error_msg(Format, {Descriptor, Arg1, Arg2, ....})
そして、{Descriptor, Arg1,...}
の行がエラーメッセージドキュメントに追加されることを確認してください。
8.4 すベてのメッセージ中の原則的なデータ構造をドキュメント化する
異なるシステムの一部の間でメッセージを送信するとき、原則的なデータ構造としてタグ付きタプルを使用します。
Erlangのレコード機能(Erlangバージョン4.3以降で導入)は、モジュール間でデータ構造の整合性を保証するのに使用されます。
これら全てのデータ構造の英語の説明はドキュメント化されているべきです。(「メッセージ解説」参照)
8.5 コメント
コメントは、明らかで簡潔で不必要な言い回しを避けているべきです。最新
のコードに追従していることをチェックしてください。コメントがコードの理解を与えることをチェックしてください。コメントは英語で書かれるべきです。
モジュールについてのコメントはインデントなしで3つのパーセント文字(%%%)から始まるべきです。(「ファイルヘッダー、解説」参照)
関数についてのコメントは、インデントなしで2つのパーセント文字(%%)から始まるべきです。(「関数ごとにコメントする」参照)
Erlangコードについてのコメントは、1つのパーセント文字(%)から始まるべきです。行がコメントのみを含む場合、Erlangコードとしてインデントされます。この手のコメントは参照する文の上に配置されます。コメントが文の同じ行に配置可能な場合、これは好まれます。
%% Comment about function
some_useful_functions(UsefulArgugument) ->
another_functions(UsefulArgugument), % Comment at end of line
% Comment about complicated_stmnt at the same level of indentation
complicated_stmnt,
......
8.6 関数ごとにコメントする
ドキュメントで大事なことは:
- 関数の目的。
- 関数の妥当な入力の範囲。つまり、関数への引数のデータ構造とその意味。
- 関数の出力の範囲。つまり、考えられるすべての返り値のデータ構造とその意味。
- 複雑なアルゴリズムを実装する関数の場合、その説明。
-
exit/1
,throw/1
もしくは非自明なランタイムエラーによって引き起こされる失敗やexitシグナルの考えられる要因。失敗とエラーを返すことの違いに注意してください。 - 関数の副作用。
例:
%%----------------------------------------------------------------------
%% Function: get_server_statistics/2
%% Purpose: Get various information from a process.
%% Args: Option is normal|all.
%% Returns: A list of {Key, Value}
%% or {error, Reason} (if the process is dead)
%%----------------------------------------------------------------------
get_server_statistics(Option, Pid) when pid(Pid) ->
......
8.7 データ構造
レコードはプレーンテキストと一緒に定義されるべきです。例:
%% File: my_data_structures.h
%%---------------------------------------------------------------------
%% Data Type: person
%% where:
%% name: A string (default is undefined).
%% age: An integer (default is undefined).
%% phone: A list of integers (default is []).
%% dict: A dictionary containing various information about the person.
%% A {Key, Value} list (default is the empty list).
%%----------------------------------------------------------------------
-record(person, {name, age, phone = [], dict = []}).
8.8 ファイルヘッダー、コピーライト
ソースコードの各ファイルはコピーライト情報で始まらなければなりません。例:
%%%---------------------------------------------------------------------
%%% Copyright Ericsson Telecom AB 1996
%%%
%%% All rights reserved. No part of this computer programs(s) may be
%%% used, reproduced,stored in any retrieval system, or transmitted,
%%% in any form or by any means, electronic, mechanical, photocopying,
%%% recording, or otherwise without prior written permission of
%%% Ericsson Telecom AB.
%%%---------------------------------------------------------------------
8.9 ファイルヘッダー、改訂履歴
ソースコードの各ファイルは、誰がファイルを操作し、何をしたかを示す改訂履歴をドキュメント化していなければなりません。
%%%---------------------------------------------------------------------
%%% Revision History
%%%---------------------------------------------------------------------
%%% Rev PA1 Date 960230 Author Fred Bloggs (ETXXXXX)
%%% Intitial pre release. Functions for adding and deleting foobars
%%% are incomplete
%%%---------------------------------------------------------------------
%%% Rev A Date 960230 Author Johanna Johansson (ETXYYY)
%%% Added functions for adding and deleting foobars and changed
%%% data structures of foobars to allow for the needs of the Baz
%%% signalling system
%%%---------------------------------------------------------------------
8.10 ファイルヘッダー、解説
各ファイルは、ファイルに含まれるモジュールの短い解説とすべてのエクスポートされた関数についての簡単な解説で始まらなければなりません。
%%%---------------------------------------------------------------------
%%% Description module foobar_data_manipulation
%%%---------------------------------------------------------------------
%%% Foobars are the basic elements in the Baz signalling system. The
%%% functions below are for manipulating that data of foobars and for
%%% etc etc etc
%%%---------------------------------------------------------------------
%%% Exports
%%%---------------------------------------------------------------------
%%% create_foobar(Parent, Type)
%%% returns a new foobar object
%%% etc etc etc
%%%---------------------------------------------------------------------
**弱点、バグ、十分にテストされていない機能を知っている場合、特別なコメントで書き留め、隠そうとしないでください。モジュールのいずれかの部分が不完全な場合は、特別なコメントを追加してください。**モジュールの機能のメンテナーに役立つものについてコメントを追加してください。作成したモジュールのプロダクトが成功した場合、10年後に会ったこともない人が変更し、改良するかもしれません。
8.11 古いコードをコメントアウトせず削除する
改訂履歴に趣旨のコメントを加えてください。ソースコードコントロールシステムが助けてくれることを忘れないでください!
8.12 ソースコードコントロールシステムを使用する
すべてのささいでないプロジェクトは、すべてのモジュールを追跡するためにRCS、CVSやClearcaseのようなソースコードコントロールシステムを使用しなければなりません。
9 もっとも一般的なミス
- 多くのページにまたがる関数を書く(「長大な関数を書かない」参照)
- if、receive、caseなどで深くネストされた関数を書く(「深くネストされたコードを書かない」参照)
- 型が正しくない関数を書く(「タグ付き返り値を使用する」参照)
- 関数が行うことを反映していない関数名(「関数名」参照)
- 意味のない変数名(「変数名」参照)
- 必要のない時にプロセスを使用する(「システム内の実際の同時アクティビティごとに1つの並列プロセスを割り当てる
- 不適切に選択されたデータ構造(不適切な表現)
- 不適切なコメント、または、コメントなし(常に引数と返り値はドキュメント化する)
- インデントされていないコード
- put/getを使用する(「細心の注意を払ってプロセス辞書を使用する」参照)
- メッセージキューの制御なし(「未知のメッセージはフラッシュする」と「タイムアウト」参照)
10 要求ドキュメント
このセクションはErlangを使用してプログラムされたシステムのデザインとメンテナンスのために必要となるシステムレベルドキュメントのいくつかについて記述します。
10.1 モジュール解説
モジュールごとに一つの章です。各モジュールの説明と、次のエクスポートされたすべての関数が含まれています。
- 関数の引数のデータ構造とその意味
- 返り値のデータ構造とその意味
- 関数の目的
- 明示的に呼ばれる
exit/1
により起こされるかもしれない失敗とexitシグナルによる考えらる原因。
ドキュメントのフォーマとはあとで定義されます。
10.2 メッセージ解説
1つのモジュール内で定義されたものを除く、すベてのプロセス間メッセージのフォーマット。
後で定義されるドキュメントの形式:
10.3 プロセス
システムに登録されるすべてのサーバーとそのインターフェイスと目的の説明。
動的プロセスとそのインターフェイスのの説明。
後で定義されるドキュメントの形式:
10.4 エラーメッセージ
エラーメッセージの説明
後で定義されるドキュメントの形式: