ErlangでHaskell風の Maybeモナド と do記法 を使う方法。エラーチェックなどを繰り返し行うコードが簡潔に記述できる。RabbitMQの開発者が公開した [erlando] (https://github.com/rabbitmq/erlando) で実現する。
erlandoにはListモナドやStateモナドなども含まれており、もちろん自分で新たにモナドを書くこともできる。また一部のモナドではMonadPlus型も実装されており guard/2
関数などが使える。さらに、関数の部分適用を読みやすく記述できる「cut」や、関数のインポート時に関数名を変更することで名前の衝突を避ける「import as」という機能もある。
maybe(A) 型
Erlang/OTPでは処理が失敗するかもしれない関数は、失敗したことを「例外」ではなく、「戻り値」で表現する。例えば lists:keyfined/3
ではキーが見つからない場合 false
を、 file:open/2
では {error, Reason}
を返す。
> lists:keyfind(a, 1, [{a, alice}, {b, bob}]).
{a,alice}
> lists:keyfind(c, 1, [{a, alice}, {b, bob}]).
false
> file:open("a", [read,raw]).
{error,enoent}
maybe(A) 型はこれを汎用化したもので、HaskellやScalaなどの言語では標準ライブラリに入っている。処理の成功時は値を {just, _}
で包んで返し、失敗時は単に nothing
を返す。
-type maybe(A) :: {just, A} | nothing.
ここでは例として Learn You a Haskell for Great Good! の [Walk the Line] (http://learnyouahaskell.com/a-fistful-of-monads#walk-the-line) を Erlang に移植してみる。養魚場で働くピエールが綱渡りに挑戦するが、バランス棒の左右にとまる鳥に邪魔されるという内容だ。棒の左右にとまった鳥の数の差が3以内であればピエールはバランスを保ち、4以上になるとバランスを崩して落ちてしまう。
なお Learn You a Haskell for Great Good! の日本語翻訳版「 すごいHaskell たのしく学ぼう! 」は [こちら] (http://www.amazon.co.jp/dp/4274068854) から購入できる。
まずは型定義。鳥の数を birds
型、バランス棒の状態を pole
型として定義する。
-type birds() :: integer().
-type pole() :: {birds(), birds()}.
pole
タプルの1つ目の値は棒の左側にとまった鳥の数、2つ目の値は右側の鳥の数を表す。
次は関数のスペック。 land_left/2
は棒の左側に鳥が着陸、 land_right/2
は右側に着陸することを表す。
-spec land_left(birds(), pole()) -> maybe(pole()).
-spec land_right(birds(), pole()) -> maybe(pole()).
1つ目の引数は鳥の数で、正の値なら鳥がとまったと考え、負の値なら鳥が飛んだと考える。2つ目の引数は現在の棒の状態を表す。検査の結果、棒の左右の鳥の差が3以内なら {just, pole()}
を、それ以外ならバランスを崩して落ちたとして nothing
を返す。
> walkline:land_left(1, {0, 0}).
{just,{1,0}}
> walkline:land_left(4, {0, 0}).
nothing
これらの関数の実装は以下の通り。
land_left(N, {Left, Right}) ->
case abs((Left + N) - Right) < 4 of
true ->
{just, {Left + N, Right}};
false ->
nothing
end.
land_right(N, {Left, Right}) ->
case abs(Left - (Right + N)) < 4 of
true ->
{just, {Left, Right + N}};
false ->
nothing
end.
モナドなしで普通に書いてみる
最初に鳥が棒の左に2羽とまり、次に右に2羽とまり、最後に左に1羽とまるケースを実装する。失敗する可能性を考慮しながら書くと以下のようになる。
-spec routine1() -> maybe(pole()).
routine1() ->
case {just, {0,0}} of
{just, Start} ->
case land_left(2, Start) of
{just, First} ->
case land_right(2, First) of
{just, Second} ->
land_left(1, Second);
nothing ->
nothing
end;
nothing ->
nothing
end;
nothing ->
nothing
end.
Erlangでは典型的なコードだ。実行してみよう。
> walkline:routine1().
{just,{3,2}}
綱渡りに成功したので just
が返ってきた。
Maybeモナドとdo記法
モナドを使って上のコードを簡潔にしよう。 routine1/0
は条件判定と値の引き継ぎ方( {just, Start}
など)は各ステップで同じで、それを繰り返している。このパターンを抽象化すればよい。
ここで活躍するのが Maybeモナドだ。erlandoでは maybe_m
モジュールとして用意されていて、そこでは、連結関数 '>>='/2
や、注入関数 return/1
などが定義されている。
-behaviour(monad).
-type(monad(A) :: {'just', A} | nothing).
'>>='({just, X}, Fun) -> Fun(X);
'>>='(nothing, _Fun) -> nothing.
return(X) -> {just, X}.
fail(_X) -> nothing.
連結関数 '>>='/2 はmaybe(A) 型のモナド値を受け取り、値がもし {just, 値}
だったらその値に関数 Fun
を適用する。 Fun
は land_left/2
のように「普通の値を受け取ってなんらかの計算を行い、モナド値を返す」関数だ。また、もし受け取ったモナド値が nothing
だったら関数の適用はせず nothing
を返す。関数を適用しようにも値がないので自然な振る舞いといえる。
注入関数 return/1
は普通の値を受け取って {just, _}
で包む関数だ。一般的なプログラミング言語の return
では処理を打ち切って値を返すために使うが、モナドの return/1
は処理を打ち切らない。この2つは名前がたまたま同じだけで、全くの別物であることに注意してほしい。
fail/1
は失敗を表す値を返す。
HaskellのMaybeモナドでも同じ関数が定義されている。routine1/0
と同じ処理をHaskellで書くとこうなる。
> return (0,0) >>= landLeft 2 >>= landRight 2 >>= landLeft 1
Just (3,2)
>>=
による関数の連鎖の中で、モナド値が左から右に流れていくイメージだ。
ErlangでもMaybeモナドの関数と、無名関数( fun() -> ... end
)を組み合わせれば動くコードが書ける。しかしErlangは関数の中置構文、遅延評価、関数の部分適用などの機能を持たないので、非常に読みづらくなってしまう。そこで専ら「do記法」というHaskellのモナド専用の糖衣構文を真似たものを使う。
do記法はもちろんErlang標準の構文ではない。erlandoではparse transformというErlangのメタプログラミング機能を使ってdo記法を実現している。そのため、do記法で書いた式は、コンパイル時に普通のErlang式に変換される。
先の routine1/0
をdo記法で書き直そう。 -compile
文で do
を使うことを宣言する。処理は do
ブロックの中に書く。
-compile({parse_transform, do}).
-spec routine2() -> maybe(pole()).
routine2() ->
do([maybe_m ||
Start <- return({0, 0}),
First <- land_left(2, Start),
Second <- land_right(2, First),
land_left(1, Second)
]).
とても読みやすくなった。実行してみよう。
> walkline:routine2().
{just,{3,2}}
routine3/0
は途中で失敗するケースだ。
-compile({parse_transform, do}).
-spec routine3() -> maybe(pole()).
routine3() ->
do([maybe_m ||
Start <- return({0, 0}),
First <- land_left(1, Start),
Second <- land_right(4, First),
Third <- land_left(-1, Second),
land_right(-2, Third)
]).
> walkline:routine3().
nothing
なお、routine2/0
をdo構文を使わずに Erlang 標準の構文で書くとこのようになる。読みにくいが、モナドの動作を理解するには役立つ。 routine2/0
もコンパイル時にこれに近い内部表現に変換されているはずだ。
-import(maybe_m, ['>>='/2, return/1]).
-spec routine4() -> maybe(pole()).
routine4() ->
'>>='(return({0, 0}),
fun(Start) ->
'>>='(land_left(2, Start),
fun(First) ->
'>>='(land_right(2, First),
fun(Second) ->
land_left(1, Second)
end)
end)
end).
errorモナド
erlandoのerrorモナド error_m.erl
はmaybeモナドにそっくりだが、モナド値としてMaybe型を受け取る代わりに、OTPでよく使われている ok | {ok, _} | {error, _}
を受け取る。
以下のコードは https://github.com/rabbitmq/erlando のREADMEより引用
使用前:Erlang標準の書き方
write_file(Path, Data, Modes) ->
Modes1 = [binary, write | (Modes -- [binary, write])],
case make_binary(Data) of
Bin when is_binary(Bin) ->
case file:open(Path, Modes1) of
{ok, Hdl} ->
case file:write(Hdl, Bin) of
ok ->
case file:sync(Hdl) of
ok ->
file:close(Hdl);
{error, _} = E ->
file:close(Hdl),
E
end;
{error, _} = E ->
file:close(Hdl),
E
end;
{error, _} = E -> E
end;
{error, _} = E -> E
end.
make_binary(Bin) when is_binary(Bin) ->
Bin;
make_binary(List) ->
try
iolist_to_binary(List)
catch error:Reason ->
{error, Reason}
end.
使用後:errorモナドを使用
write_file(Path, Data, Modes) ->
Modes1 = [binary, write | (Modes -- [binary, write])],
do([error_m ||
Bin <- make_binary(Data),
Hdl <- file:open(Path, Modes1),
Result <- return(do([error_m ||
file:write(Hdl, Bin),
file:sync(Hdl)])),
file:close(Hdl),
Result]).
make_binary(Bin) when is_binary(Bin) ->
error_m:return(Bin);
make_binary(List) ->
try
error_m:return(iolist_to_binary(List))
catch error:Reason ->
error_m:fail(Reason)
end.
その差は歴然としている。なお、このように do
ブロックを入れ子にすることで、内側の do
ブロックでファイルを書き込んでいる最中にエラーが起こっても、外側の do
ブロックが確実にファイルをクローズできる。
導入方法
ビルドツールの [rebar] (https://github.com/rebar/rebar) を使うとよいだろう。
プロジェクトの rebar.config
に依存関係を追加する。
{deps, [{erlando, ".*", {git, "git://github.com/rabbitmq/erlando.git", {branch, "master"}}}]}.
get-deps
でerlandoを取得し、 compile
でプロジェクト全体をビルドする。
$ ./rebar get-deps
$ ./rebar compile
Erlang VMではerlandoをパスに追加すればOK。
$ erl -pz ebin/ -pz deps/erlando/ebin/
rebarの使い方は、こちらで紹介されている。
とり日記 -- 2012/07/10(火) Erlang解説(19) - rebar -
http://www.avian-net.jp/blog/sakada/053