list_max/1関数作成
list_max(L)関数はひとつの数字のリストを受け取って、その中の一番大きい数字を返す関数です。まず骨組みを作りたいです。funs.erlファイルを作成します。
-module(funs).
-export([list_max/1]).
list_max([H|T]) ->
list_max(H, T).
list_max(M, [H|T]) when H > M ->
list_max(H, T).
funs.erlをコンパイルして、実行します。
$ erlc funs.erl
$ erl
1> funs:list_max([1,2,3]).
** exception error: no function clause matching funs:list_max(3,[]) (funs.erl, line 15)
エラーが出てきました。なぜかというと再帰的にlist_max(H,T)関数が呼び出されていくと、いずれTが空になります。Tが空になった場合はそのようなパターンを処理するコードが書かれていないからです。
2行を追加すればこのエラーを解消することができます。
-module(funs).
-export([list_max/1]).
list_max([H|T]) ->
list_max(H, T).
list_max(M, []) ->
M;
list_max(M, [H|T]) when H > M ->
list_max(H, T).
上のコードをコンパイルして、実行してみると、エラーがなくなりましたが、リストの要素1と2の位置を逆にすると、また別のエラーが出てきました。
$ erl
1> funs:list_max([1,2,3,4]).
4
2> funs:list_max([2,1,3,4]).
** exception error: no function clause matching funs:list_max(2,[1,3,4]) (funs.erl, line 13)
なぜかというとlist_max()関数の中にガードを使って H>Mという分岐しか書かれていないからです。
上の[1,2,3,4]の例だと2(H) > 1(M) なのでlist(2,[3,4])を次の再帰で呼び出されます。しかし、リストが[2,1,3,4]の場合、H(1) < M(2)はあるいは H == Mの状況の処理が書かれていないです。
なので、二行を追加します。
-module(funs).
-export([list_max/1]).
list_max([H|T]) ->
list_max(H, T).
list_max(M, []) ->
M;
list_max(M, [H|T]) when H > M ->
list_max(H, T);
list_max(M, [_H|T]) ->
list_max(M, T).
一番下の2行は H <= M のときにそこで処理が行われます。実行してみるエラーがなくなりました。
$ erlc funs.erl
$ erl
1> funs:list_max([2,1,3,4]).
4
引き続き空のリストをlist_max()に渡したら、どうなりますか?
$ erlc funs.erl
$ erl
1> funs:list_max([]).
** exception error: no function clause matching funs:list_max([]) (funs.erl, line 10)
つまり空のリストを処理するコードが書かれていないからです。funs.erlの上のほうに2行を追加したらエラーを解消します。
-module(funs).
-export([list_max/1]).
list_max([]) ->
void;
list_max([H|T]) ->
list_max(H, T).
list_max(M, []) ->
M;
list_max(M, [H|T]) when H > M ->
list_max(H, T);
list_max(M, [_H|T]) ->
list_max(M, T).
これで実行してみるとvoidを返すようになりました。
$ erlc funs.erl
$ erl
1> funs:list_max([]).
void
これでlist_max()関数も一段完了しました。
単体テスト
funs.erl ファイルのlist_max()関数を利用して、単体テストを実施したいです。
まずはfuns_tests.erl を作成して、テンプレートを作ります。
-module(funs_tests).
-include_lib("eunit/include/eunit.hrl").
list_max_test() ->
?assertEqual(void, funs:list_max([])),
?assertEqual(1, funs:list_max([1])),
?assertEqual(3, funs:list_max([1,2,3])),
?assertEqual(4, funs:list_max([2,1,4,3])).
そして、funs_test.erlファイルをコンパイルして、単体テストを実行します。
$ erlc funs_tests.erl
$ erl
5> eunit:test(funs).
Test passed.
ok
このfuns_testsモジュールについて、いくつのポイントを説明します。
funs.erlモジュールをテストしたいので、eunit test関数は自動的にfuns_testsモジュールを探します。つまりテストモジュールは'_tests'付けの決まった名前を使用しないといけません。
-include_lib()でeunitのヘッダーファイルをインクルードする必要があります。
funs.erlファイルのlist_max()をテストしたいので、funs_tests.erlの中の関数名も'_test'で終わる必要があります。
注意していただきたいのは実際の開発手順としてはテストケースを書いて、コーディングしますが、説明のため順序は逆になっています。
Client/Serverモデル
funs.erlファイルのlist_max()を利用して、Client/Serverの仕組みを導入したいです。
その中でクライアントは新しく定義したRPC関数(Remote procedure call)を使って、サーバーに問い合わせます(リスト要素の最大値)。まずfuns_server.erlファイルを作成します。
-module(funs_server).
-export([rpc/2]).
rpc(Pid, Request) ->
Pid ! {self(), Request},
receive
Response ->
Response
end.
fun_serverサーバーがクライアントのためにrpc/2関数を用意しました。
rpc(Pid, Request)の中のPidはサーバーのプロセスIDのことです。クライアントはrpc/2を使ってPidに '{self(), Request}'という形の問い合わせを送ります。
self()クライアントのプロセスIdのことです。self()はなぜ必要なのかというと、問い合わせを送ったら、サーバーがその問い合わせに対する返事はどこに(クライアントに)送るかを伝える役割です。
問い合わせを送ったら、返事を待つ状態になります。つまりreceive...end文に入ります。
次に本当のサーバー(loop関数)を作ります。
-module(funs_server).
-export([rpc/2, loop/0]).
rpc(Pid, Request) ->
Pid ! {self(), Request},
receive
Response ->
Response
end.
loop() ->
receive
{From, L} ->
From ! funs:list_max(L),
loop()
end.
funs_server.erlをコンパイルして、実行してみます。
1> Pid = spawn(fun funs_server:loop/0).
<0.35.0>
2> funs_server:rpc(Pid, [2,1,4,3]).
4
まずerlシェル(クライアントとも言える)からサーバーを起動するためにspawn()関数を使って、ひとつのプロセスを作ります。
このプロセスに実行しているのはloop()関数で、つまりサーバーのことです。
サーバーのPidは<0.35.0>で、クライアントがサーバーが用意してくれたrpc/2関数を使って問い合わせして、結果はリストの最大値4が戻ってきました。
loop()関数が何をしたかというとクライアントからの問い合わせてずっと待っている状態で、きたら、上の作ったfuns.erlモジュールのlist_max()関数を呼び出して、結果をクライアント(erlシェル)に返します。そしてもう一回loop()関数に入って、クライアントからの問い合わせを待つ状態になります。
毎回クライアントがspawn()を使って、サーバープロセスを作るなんてあほらしいなので、start()関数にラップしてしまいます。
-module(funs_server).
-export([start/0, find_list_max/2, loop/0]).
start() -> spawn(fun loop/0).
find_list_max(Pid, What) ->
rpc(Pid, What).
rpc(Pid, Request) ->
Pid ! {self(), Request},
receive
Response ->
Response
end.
loop() ->
receive
{From, L} ->
From ! funs:list_max(L),
loop()
end.
上のコードはstart/0とfind_list_max/2関数を追加しました。start/0関数を追加する理由は述べましたが、find_list_max/2はなぜ必要なんでしょうか?
理由はクライアントは直接rpcを使わずに、後で、rpcの仕様変更があったら、クライアントもいちいちrpcの引数にあわせて、問い合わせを送らないといけませんからです。
find_list_max/2関数がrpcをラップしてしまうとrcpの仕様が変更してもクライアントには関係ないです。といっても、find_list_maxを設計している段階で、ちゃんと引数への考量は大事です。
Makefile作成
今までの流れで、もう三つの'.erl'ファイルがありました。変更するたびに長いコンパイルコマンドを打つのは面倒なので、Makefileに集約します。Makefileを作ります。
.SUFFIXES: .erl .beam
.erl.beam:
erlc -W $<
ERL = erl -boot start_clean
MODS = funs funs_tests funs_server
all: compile
${ERL}
compile: ${MODS:%=%.beam}
clean:
rm -rf *.beam erl_crash.dump
これでコマンドラインにmakeコマンドを打ってすべてをコンパイルしてくれます。
$ make
いくつか注意点で
* Makefileの中にスペースを使わないこと
* 'erlc -W'の'-W'はワーニングを有効にすること
* 'MOD ='の後ろに続いているのはコンパイルしたい'.erl'ファイル('.erl'なし)
* Makefileの中にall, compile, cleanなどたくさんのターゲットを出てきます。
* コマンドラインに '\$ make [Target]'を打って、[Target]を省略した場合は最初に出てくるターゲットが使われます。このMakefileの場合は $ make all になります
str_word_count単語をカウント
funs.erlファイルの中にstr_word_count/1関数を作ります。文字列をstr_word_count/1を渡して、スペースで区切られている単語数をカウントする関数をfuns.erlファイルに追加します。まず骨組みを作ります。
引数2個目の'0'は現在カウントした単語数を表すアキュムレータです。
str_word_count(Input) ->
str_word_count(Input, 0).
この関数は再帰を使いますので、最後に文字列は空になるとアキュムレータを返す処理を追加します。
str_word_count(Input) ->
str_word_count(Input, 0).
str_word_count([], Count) ->
Count;
単語をカウントするやり方としては文字列の中に単語とスペースの境界線になると単語を一個をカウントします。その処理もコードの中に追加します。'$\'はErlangの中でスペースを意味しています。
str_word_count(Input) ->
str_word_count(Input, 0).
str_word_count([], Count) ->
Count;
str_word_count([First, Second | Tail], Count) when First =/= $\, Second =:= $\ ->
str_word_count([Second|Tail], Count+1);
文字列の中に単語とスペースの境界線に会う以外の場合はそのまま再帰的に関数を呼び出します。その処理もコードの中に追加します。
str_word_count(Input) ->
str_word_count(Input, 0).
str_word_count([], Count) ->
Count;
str_word_count([First, Second | Tail], Count) when First =/= $\, Second =:= $\ ->
str_word_count([Second|Tail], Count+1);
str_word_count([ _ | Tail], Count) ->
str_word_count(Tail, Count).
上のコードはほぼ完了ですが、最後に追加する内容は文字列最後の文字がきたら、処理しないといけません。最後の文字はスペースではなかったら、アキュムレータの単語も1をプラスします。そのコードも追加します。
str_word_count(Input) ->
str_word_count(Input, 0).
str_word_count([], Count) ->
Count;
str_word_count([Last], Count) when Last =/= $\ ->
Count+1;
str_word_count([First, Second | Tail], Count) when First =/= $\, Second =:= $\ ->
str_word_count([Second|Tail], Count+1);
str_word_count([ _ | Tail], Count) ->
str_word_count(Tail, Count).
注意してほしいのは文字列(例えば "I love you")の最後の文字はスペースではないなら、下のコードは必要ない
str_word_count([], Count) ->
Count;
しかし、最後の文字はスペースだったら(例えば:"I love you ")必要です。
これでコンパイルして、実行してみます。
$ make
$ erl
1> funs:str_word_count("I love you ").
3
2>
file_count_chars/1関数作成
Erlangのファイル操作を勉強するために、funs.erlファイルの中にfile_count_chars/1関数を作ります。
この関数に引数として、ファイル名を渡して、そして、ファイルの中の文字'x'の数を返してくれます。
まず骨組みを作ります。
file_count_chars(Fname) ->
case file:open(Fname, [read, raw, binary]) of
{ok, Fd} ->
%% 処理コード
{error, Reason} ->
%% 処理コード
end.
file:open/2関数は成功するときに{ok, IoDevice}を返します。失敗するときに {error, Reason}を返します。
ファイルを二つのモードで開くことができます。ひとつはbinary、もうひとつはnormal。
rawオプションは早いですが、'io'モジュールは使えません。その代わりにread/2、 read_line/1 とwrite/2 関数を使います。
次に用意する関数はscan_file/3です。この関数は最後にファイルの文字数を返します。使い方下のようです。
'Acc'はアキュムレータ、つまりscan_fileは1Kのデータを読み込んで、なんらかのヘルパー関数(count_x/1)でその1Kデータの文字列を統計して、それをAccで表示します。
scanf_file(Fd, Acc, file:read(Fd, 1024))
これでコードを追加します。
file_count_chars(Fname) ->
case file:open(Fname, [read, raw, binary]) of
{ok, Fd} ->
Res = scanf_file(Fd, 0, file:read(Fd, 1024)),
file:close(Fd),
Res;
{error, Reason} ->
{error, Reason}
end.
scan_file(Fd, Acc, {ok, Binary}) ->
%% 処理コード
scan_file(Fd, Acc, eof) ->
file:close(Fd),
Acc;
scan_file(Fd, Acc, {error, Reason}) ->
file:close(Fd),
{error, Reason}.
file:read/3関数の戻り値は三つのパターンがあります。{ok, Data}、 eof と {error, Reason}です。
ファイルの最後に達したら、ファイルを閉じて、1Kデータの中の文字数Accを返します。
最後にscan_file/3の中に使うヘルパー関数(count_x/1)を追加します。
file_count_chars(Fname) ->
case file:open(Fname, [read, raw, binary]) of
{ok, Fd} ->
Res = scanf_file(Fd, 0, file:read(Fd, 1024)),
file:close(Fd),
Res;
{error, Reason} ->
{error, Reason}
end.
scan_file(Fd, Acc, {ok, Binary}) ->
scan_file(Fd, Acc + count_x(Binary), file:read(Fd, 1024));
scan_file(Fd, Acc, eof) ->
file:close(Fd),
Acc;
scan_file(Fd, _Acc, {error, Reason}) ->
%% ファイルの最後に到達
file:close(Fd),
{error, Reason}.
%% ヘルパー関数
count_x(Binary) ->
count_x(binary_to_list(Binary), 0).
count_x([], Acc) ->
Acc;
count_x([$x|Tail], Acc) ->
count_x(Tail, Acc+1);
count_x([_|Tail], Acc) ->
count_x(Tail, Acc).
count_x/1がデータを統計する前にまずfile:read()から取得した1Kのバイナリデータをリストに変更する必要があります。