LoginSignup
25
23

More than 5 years have passed since last update.

erlando - ErlangでMaybeモナドとdo記法を使う

Posted at

ErlangでHaskell風の Maybeモナドdo記法 を使う方法。エラーチェックなどを繰り返し行うコードが簡潔に記述できる。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 を返す。

walkline.erl
-type maybe(A) :: {just, A} | nothing.

ここでは例として Learn You a Haskell for Great Good!Walk the Line を Erlang に移植してみる。養魚場で働くピエールが綱渡りに挑戦するが、バランス棒の左右にとまる鳥に邪魔されるという内容だ。棒の左右にとまった鳥の数の差が3以内であればピエールはバランスを保ち、4以上になるとバランスを崩して落ちてしまう。

なお Learn You a Haskell for Great Good! の日本語翻訳版「 すごいHaskell たのしく学ぼう! 」は こちら から購入できる。

まずは型定義。鳥の数を birds 型、バランス棒の状態を pole 型として定義する。

walkline.erl
-type birds() :: integer().
-type pole() :: {birds(), birds()}.

pole タプルの1つ目の値は棒の左側にとまった鳥の数、2つ目の値は右側の鳥の数を表す。

次は関数のスペック。 land_left/2 は棒の左側に鳥が着陸、 land_right/2 は右側に着陸することを表す。

walkline.erl
-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

これらの関数の実装は以下の通り。

walkline.erl
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羽とまるケースを実装する。失敗する可能性を考慮しながら書くと以下のようになる。

walkline.erl
-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 などが定義されている。

maybe_m.erl
-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 を適用する。 Funland_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 ブロックの中に書く。

walkline.erl
-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 は途中で失敗するケースだ。

walkline.erl
-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 もコンパイル時にこれに近い内部表現に変換されているはずだ。

walkline.erl
-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 を使うとよいだろう。

プロジェクトの 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

25
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
23