こんにちは、kojingharang です。仕事でErlangを使い始めてそろそろ2年になります。
家でPCをいじっているときはだいたい 馬場タクシー3D や 他のアプリ を作っていますが、最近区切りが良くてちょっと暇になったので、アドベントカレンダーに乗っかって Erlang の記事を書いてみます。
対象読者
- Erlang の軽量プロセスやクラスタサポートは気に入っているが、その他のいろいろについて、もやっとした不満がある方
要約
こんな流れです。ここだけ見て「もう分かったよ」という方は、続きは見なくても大丈夫です。
- 足し算するモジュールを書こう
- それを gen_server 化しよう
- 冗長な記述をしなきゃいけなくて面倒だしバグがでそうだ
- そこで、面倒な記述を parse_transform で自動生成する fun_injector というのを作ったよ
- 冗長な記述が減ってハッピー!
- 考察
足し算するモジュールを書こう
突然ですが、あなたは Erlang プログラマーとして D◯◯◯g◯ 社に採用された若きホープです。
さっそく上司が
「我が社が 1 兆円くらい儲かるすごいモジュールを書いて欲しい。あ、分からないことがあったら何でも聞いてね。」
と無茶振りしてきました。
なるほど?さぁ腕の見せどころです。
あなたは「数字を状態として持って、それに対してなんと足し算することができちゃうモジュール」を書きました。
(例なので単純ですが、このモジュールは読者のみなさんが書いている複雑なビジネスロジックだと思ってください...)
それを gen_server 化しよう
足し算モジュールを見せたら、上司が
うむ、これはいける。
さっそく製品に組み込むにあたって、いろんな利用者が足し算の状態を共有したいから、独立プロセスにしたいね。
と言い出しました。なるほど?
そこで、あなたは「さっきのモジュールをラップする gen_server」を書くことにしました。
ロジックモジュールそのものと gen_server behaviour 実装モジュールを別にしたことで、ロジックモジュールそのままでも、gen_server としてでも、よくテストされた足し算ロジックが使えるようになりました。
また、gen_server 実装モジュールに adder のラッパー関数を定義したので、利用者側は足し算モジュールを正しく使えているか、dialyzer の型チェックにより検証できるようになりました。
どういうことかというと、もしラッパー関数(adder_server_handwritten:add/2
) がなくて利用者が直接
gen_server:call(Pid, {add, 2})
を呼ぶようになっていると、利用者が間違って
gen_server:call(Pid, {plus, 2})
と書いてもコンパイルが通ってしまうので間違いに気付かないかもしれないわけです。(テストカバレッジが100%なら問題ないですがね!)
また、足し算モジュールの仕様が変わって
gen_server:call(Pid, {add, 2, LogOption})
と書かなきゃいけなくなったのに利用者側のコードは今まで通りだったとしても、やっぱりコンパイルが通ってしまいます。
ラッパー関数とその spec を書くことで dialyzer のエラーが出るようになるので、他チームからの「動かないんだけど...」という問い合わせが減り、あなたのチームには平和が訪れました。
冗長な記述をしなきゃいけなくて面倒だしバグがでそうだ
しかし、あなたは今一度さっきのコードを見て、 冗長な記述 が多いことに気づきました。
(実は書きながら思っていたわけですが...)
ていうか、ラッパー関数内で call するときのタプルの個数とか、いかにも間違えそうな気がします。そしてそれはコンパイルエラーでは分かりません。
問題が他チームから自チームに移動しただけだったのです。
ですが、問題が集約されたので、状況は前より良くなっています。もう一工夫です。
そこで、面倒な記述を parse_transform で自動生成する fun_injector というのを作ったよ
そこで、めんどくさいことが嫌いなあなたは、@kojingharang という暇な人が作った fun_injector というツールを使って、さっきの冗長な記述を自動生成することにしました。
そのツールを使うと、こんな記述
-compile([{parse_transform, fun_injector},
{fun_injector_extract_from, adder}]).
を空の gen_server 実装モジュールに追加するだけで、コンパイル直前にラッパー関数が生成され、さっきのgen_server と同等の gen_server 実装モジュールが出来上がります。
テストもばっちりです。
gen_serverのその他の処理を書きたい時は、普通に handle_call や handle_cast などの処理を書くことができます。
ちなみに parse_transform というのは Lisp のマクロのようなもので、Erlang コンパイラのオプションとして
{parse_transform, Module}
を(ソースに書くなり erlc の引数に渡すなりして)コンパイラに渡すと Module:parse_transform/2
にコードの AST を渡して、返ってきた結果をコンパイルしてくれるやつです。AST を自由にいじることができるので行番号とかを出力するためにロガーの lager とかで使われています。
冗長な記述が減ってハッピー!
というわけで、fun_injector を使ったところ、冗長な記述なしにコードの型チェックができるようになったので生産性が上がってハッピーというお話でした。
1 兆円儲かるかどうかは今後に期待です。
考察
書くのが面倒になったので最後投げやりにまとめておきます。
- 対象となるモジュールの関数の型は
init(A, B, ...) -> {ok, State}
またはXXX(A, B, ..., State) -> {X, Y, ..., State}
である必要がある。このあたりは Erlang が OOP のクラス的な記述をサポートしてないため、thisやselfに相当するものは何か、という共通の決まりがないため。- Elixir はこのあたりどうなんだろう。
- この手のコード生成とか自動◯×ツールは過去にいくつか見てきたが、実際に導入するとなると結構隠れたコストが高い
- (例)生成後のコードが容易に想像できないので、動かなかった時に追いづらい/性能劣化がないことが保証できない
- (例)場合によってはコードそのものを見たいが生成されたコードは見れたものではない or 見れない
- fun_injector も同じである ... ので、とりあえず作ったものの、あんまり力を入れようとは思わない (💰 次第ですが)
- そのあたりのもろもろのコストが結構高いので、10コ程度のラッパー関数なら手動で書いた方が導入/メンテコストなどトータルで安い
- ラッパー関数が 10 コでも、そういうモジュールが 30 コくらいあって仕様もころころ変わるようなプロジェクトを少人数でやるような場合には、導入を検討してもよいかも
- 今更だけど、どっちかというと、こういうのはASTいじるよりコード生成で実現する方が生成コードが見えるという点で筋がいい気がする。ツールの役割はあくまで人間の代わりにコード生成するまでであり、生成コード含め git 管理することでツールの不透明性/不確実性をプロダクトに持ち込まないようにする。的な。ていうかこの方針はいけるかもしれない。続く。
それでは皆様よいお年を。