前書き
最近Erlangに興味を持ったので、勉強してみました。
そんな中で皆様にも役立つ(と思える)情報を少し記します。
(Erlang経験1ヶ月未満と題しましたが、実のところ15〜16年前(?)に少しだけ勉強したことがあります。しかしまあ未経験と言っていいレベルです
)
ループ処理
プログラミングすると言えばループ処理が必須ですが、Erlangには一般のプログラミングにある While, for などの繰り返し文がありません。ではどうするかというと、再帰関数呼び出しで繰り返し処理をします。
再帰といっても特に末尾再帰(Tail recursion)が重要です。関数の最後に自分自身を呼び出すと実質的には goto と同じ処理ができ、スタックを消費しないという利点があります。
再帰関数でよく使われるのが
lists:map(Func, List)-
lists:foreach(Fun, List)
あるいはもっと一般的な lists:foldl(Func, InitialVal, List)lists:foldr(Func, InitialVal, List)
があります。listsモジュールにはその他、filter, partitionなど便利な関数があります。またリスト・コンプリヘンションというのも使えます。
上記どの関数も、Listの一つ一つの要素を対象にして Func関数で処理するものです。
以上の関数を使えば、だいたい繰り返し処理は記述できます。しかし現実には途中でやめたり、リストのようなデータでなくファイルから1行ずつ処理したいというようなことが起こります。その場合いちいち専用の関数を作成する必要があります。作成してもいいのですが、(まだ再帰関数に慣れていない場合には)ループ処理に使える汎用的な関数を作っておくと便利と思います。
% ループ用汎用関数
loop(Control, Vals, Body) ->
case Control of
next -> {Cont, R}= Body(Vals), loop(Cont, R, Body);
return -> Vals
% _ -> throw exception
end.
呼び出しの引数は、
Control : next でループ継続;return で終了;(atom type)
Vals : 初期値を渡します。2つ以上の値が必要なら、{...} (tuple) で渡す。
Body : 処理本体の関数、
{next, 次の値}または{return, 戻り値} を返す。
% loop/3 関数の使い方説明
loop(next, {InitVal_1, InitVal_2}, % 最初に nextと初期値を渡す
fun({Val1, Val2}) -> % 繰り返し処理
...
case ... of
... -> {return, RetVal}; % 終了
... -> ...,
{next, {V1, V2}} % 繰り返し
end
end)
% 簡単な例
% 16進数の文字列をBinaryのバイト列に変換する。
testloop(Hexstr) ->
io:format("Start...\n"),
Bytes= loop( next, {Hexstr, <<>>},
fun({Str, Acc})-> case Str of
[] -> {return, Acc};
[H,L|Rest] ->
B= list_to_integer([H,L], 16),
{next, {Rest, <<Acc/binary, B>>}}
end
end),
io:format("~p\n", [Bytes]).
maps(レコード、構造体として使う)
Erlangには -record(recName, {field1, field2,...})という、レコード(構造体)の宣言がありますが、どうも使い勝手がよくありません。その代わりに map型のデータが使いやすいと思います。map型というのは key〜value 対を扱うデータ形式です。
同じようなデータ形式でdictモジュールがありますが、map型の方を推奨します。Key数が32個以下であれば高速でアクセス出来るようです。
% map型データの作成 例
File= #{name=>"filename",
creat=>CreationDatetime,
modif=>ModificationDatetime,
md5=> <<....>>},
% アクセス用の関数を定義しておくと便利
mget(Map, Key) -> maps:get(Key, Map, nil).
mset(Map, Key, Val) -> Map#{Key:=Val}.
ステータス
Erlangの変数は(名前とは裏腹に)一度定義すると変更できません。tupleやarrayやmap型のデータの一部を変更したら別の変数に入れて使い回すことになります(最終的には再帰呼び出しをして見かけ上変更できる)。
しかし、現在の状態が刻々と変わるデータがあって、データを変更したり、現在値を取得したい場合どうするのか…。
これには基本的にプロセスを使います(他の方法もあるようなことをどこかで読んだ気もするけれど…)。
上で再帰呼び出しを行うと変数の値を見かけ上変更できると述べました。それをした上でその関数をそのまま別のプロセスとして残しておくわけです。(プロセス〜他の言語では一般にスレッドと呼ぶもの。Erlangではプロセスごとにメモリが独立しています。プロセスはいくら作っても安上がりで、プロセス間の通信も容易にできます。)
ということで、
Pid= spawn(ModuleName, FuncName, [Params])
で、プロセスを起動します。PidはプロセスIDでこれを使って
Pid ! Message
とメッセージを送って、
receive .... (after xxx ...) end
節でメッセージを受け取り、データをやり取りします。Messageの型は自由です。
プロセス(Pid)側では、受け取ったメッセージごとに処理し、最後に再帰呼び出しをして待機します。
% ステータスを保持・更新する簡単なプロセス例
state(Val) ->
receive % メッセージ受信
{get_data, RepPid) -> % データ要求
RepPid ! Val, % 要求元に返す
state(Val); % 元の値で続行
{set_data, NewVal} -> % データ更新
state(NewVal); % 新データで再帰
terminate -> % プロセス終了
io:format("state terminated~n")
end.
データを貰いたい時は、stateプロセス側で誰に返すか教えなければならないので自分のプロセスID(RepPid、self()で取得)をMessageの中に含めておきます。
以上、根幹だけ説明しました。
Regular Expression
常套表現(もとい、正規表現です😅)は標準の reモジュールにあります。
文字列の一部を捕捉する使い方をよくします。例えば、
% 引用符"..."の中身と、その後の文字列を取り出したい場合…
Str= "xxx \"ABCD\" 12ab".
M= re:run(Str, "\"(.+?)\"\s*(.+)", [unicode]).
% とすると、
{match,[{4,11},{5,4},{11,4}]}
% が返る。
マッチした位置と長さのペアが返されます(第一のペアはマッチした全体)。
ここで気をつけなければならないのは、日本語が入っている場合です。例えば
% 同上、日本語の文字列の場合
Str2= "xxx \"あいう\" 12ab".
M2= re:run(Str2, "\"(.+?)\"\s(.+)", [unicode]).
% とすると、
{match,[{4,16},{5,9},{16,4}]}
% が返る。
{5,9}が"あいう"の部分なのですが、長さ9がおかしい。これはUTF-8エンコーディングのバイト数です。ということなので(ここは将来改善されるかも?)、文字列はUTF-8のbinaryデータ <<"日本語"/utf8>> として渡すのがよいと思います。
また位置と長さのペアより、捕捉した文字列を扱うことが多いと思いますので、文字列を返す自作の関数を作っておきましょう。
% 正規表現でマッチ(キャプチャー)した文字列を返す
% (Strには 原則としてUTF-8 binary型を渡すこと)
re_match(Str, Pattern) ->
case re:run(Str, Pattern, [unicode]) of
{match, Capts} ->
lists:map(fun({Pos,Len})-> string:slice(Str, Pos, Len) end, Capts);
nomatch -> false
end.
テキストの読み書き
日本に住んでいると日本語テキストを扱わなければなりません。
今どきはUTF-8エンコードを採用するのが普通になっていると思います。Erlangも当然UTF-8コード文字が扱えます。
とは言え、わたし自身まだ詳しく調べていませんので、現在掴んでいるところだけ述べます。
-
まずテキストデータの型として以下の二種があります。
- T1= "かな漢字" → リスト形式
- T2= <<"かな漢字"/utf8>> → UTF-8 の bytes列
- 相互変換:
T1 → T2 に変換するのは、
unicode:characters_to_binary(T1)
T2 → T1 への変換は、
unicode:characters_to_list(T2)
-
入力
・標準入力なら
io:get_line("")またはfile:read_line(standard_io)
で入力できます(ただし unicode に変換される)。
・ファイルから読み込むなら、binaryモード でopenして
file:read_line(File)またはfile:read(File, Count)
でファイル読み込みするのがよいと思う。 -
出力
・標準出力なら io:put_chars(T1) を使う。T1、T2どちらでも可能。
file:write(standard_io, T1)では badarg のエラーになる。
file:write(standard_io, T2)では表示が化ける。
・ファイルへの出力も binaryモードでopenするのが無難(か?)
あとがき
Erlang初心者でこんなときはどうすればよいか〜自分で気づいたところを記してみました。皆様の参考になれば嬉しく思います。
まだまだ初心者で説明不足のところや、分かりにくいところが多々あったかと思います。改善して行きたいと思います。
初心者のErlangのイメージとして…
- Erlangは通信の分野でよく使われている
- 大量のプロセスを同時に動かしながら処理するのが普通
- 通信が得意なので、分散システム(複数台のコンピューターで処理)に適合する
関連言語
Erlangのプログラムに慣れたら、もっと高級な(?)言語を試してみませんか。以下の言語は、Erlangのbeamファイにコンパイルされ、Erlangシステムで動きます。
- Elixir
これが一番よく知られていると思う - L.F.E.
Lisp Flavoured Erlang; Lisp言語が好きな方に - Gleam
比較的新しいプログラミング言語