田中です。お久し振りです。
TECHSCORE Advent Calendar 2015 の 9 日目の記事です。
唐突ですが、Erlang はじめました。
Erlang がどんな言語か、ざっくり言えば、こんな感じです。
・並列処理/分散処理に特化されている
・関数型言語で文法が特殊
この記事では、Erlang の始め方と、言語の雰囲気がわかるような簡単なプログラムについて書きます。少しでも Erlang に興味を持って頂ければ幸いです。
ところで、"Erlang" は「アーラング」より「アーラン」が良いでしょう。"ng" は「有声軟口蓋鼻音」です。日本語の「ん」は、後に続く音によって変化し、例えば「まんが」「さんぽ」「おんど」の「ん」は全て違う音です。この内、「まんが」の場合のように、カ行/ガ行音の前の「ん」が有声軟口蓋鼻音です。ですから「アーラング」と言うつもりで最後の「グ」を我慢するのが良いと思います。
インストール
各環境の Erlang はここから取得できます。
https://www.erlang-solutions.com/resources/download.html
筆者は CentOS 7 を使っていますが、問題なくインストールできました。
1 2 |
$ sudo rpm -Uvh http://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpm $ sudo yum install erlang |
Erlang シェルで実行
コマンド erl で Erlang シェルが起動します。対話式に Erlang が実行できるので、とりあえず、触ってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
$ erl 1> 10 + 20. % 最後は .(ピリオド) で終わるという決まり。 30 2> Foo = 1. % 変数は大文字で始める。 1 3> Foo = 2. % 変数に再代入しようとするとエラー。"参照透過性" で検索! ** exception error: no match of right hand side value 2 4> hoge. % 小文字で始まるのはアトムと呼ばれる。要するに不変のリテラル。 hoge 5> {foo, bar, baz}. % {}(波括弧) はタプル。要素は変更できない。 {foo,bar,baz} 6> [foo, bar, baz]. % [](角括弧) はリスト。要素を追加したり、色々できる。 [foo,bar,baz] 7> "ABC". % 文字列もあるように見えるが... "ABC" 8> "ABC" =:= [65, 66, 67]. % 実はただの数値のリスト(=:= は型と値が同じとき true)。 true 9> q(). % Erlang シェルを終了。 |
ファイルに書いた関数をシェルで実行
Erlang シェルでは関数の定義はできません。ファイルに関数を定義し、Erlang シェルから呼び出してみましょう。
お約束の "hello, world" です。
● mytest.erl
1 2 3 4 5 |
-module(mytest). % モジュール名はファイル名と一致させる必要がある。 -export([hello/0]). % 公開する関数をリストで指定(0 は引数の個数が 0 という意味)。 hello() -> io:format("hello, world~n"). |
このファイルを Erlang シェルでコンパイルして実行します。
1 2 3 4 5 6 7 8 |
$ erl 1> c(mytest). % c は compile の略。 {ok,mytest} 2> mytest:hello(). hello, world ok |
シェルを使わず直接コンパイルして実行
上記の方法は開発時には便利ですが、シェルなしで実行できなければ話になりませんよね。
次は、コマンドラインから直接コンパイルして実行してみましょう。
引数に名前を指定して、"Hello, ○○○!" と表示させます。
● mytest2.erl
1 2 3 4 5 6 |
-module(mytest2). -compile(export_all). % いちいち関数名を指定するのが面倒な場合はこう書く。 hello([Arg]) -> io:format("Hello, ~s!~n", [Arg]), erlang:halt(). % これがないと Erlang VM が終了しない。 |
コンパイルのコマンドは erlc です。
1 2 3 4 |
$ erlc mytest2.erl # mytest2.beam (バイトコード) が作成される。 $ erl -noshell -run mytest2 hello TECHSCORE Hello, TECHSCORE! |
スクリプトとして実行
練習のため、ちょっとしたプログラムを Erlang で書いてみたいのに、いちいち上記のようにしなければならないのは面倒ですよね。
escript を使えば、コンパイルせず、直接実行することができます。
● hello.erl
1 2 3 4 5 6 |
#!/usr/bin/env escript -module(hello). -compile(export_all). main([Arg]) -> % 関数名は main にする必要がある。 io:format("Hello, ~s!~n", [Arg]). |
実行権限を付与して実行します(勿論、escript hello.erl としても良いです)。
1 2 3 4 |
$ chmod +x hello.erl $ ./hello.erl TECHSCORE Hello, TECHSCORE! |
これなら、気軽に Erlang を使ってみよう、という気になりませんか?
再帰呼び出しで反復処理
Erlang はいわゆる関数型の言語です。for ループとかはありません。反復処理は再帰呼び出しで行います。
リストの先頭要素を head、残りの部分を tail と呼びます。Erlang は、リストの再帰処理が書き易いよう、head と tail の分解及び結合が簡単にできるように設計されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ erl 1> MyList = [1, 2, 3]. [1,2,3] 2> [Head|Tail] = MyList. % head と tail に分解。 [1,2,3] 3> Head. 1 4> Tail. [2,3] 5> [9|MyList]. % 先頭に要素を追加。 [9,1,2,3] |
では、これを使って、リストの要素を新しい要素で置き換える関数
replace(新要素, 旧要素, リスト)
を書いてみます。
● f_test.erl
1 2 3 4 5 6 7 8 9 10 11 |
-module(f_test). -compile(export_all). % replace を 3 つ定義しているように見えるが、これで 1 つの定義 replace(_, _, []) % ①リストが空の場合 -> []; % 空リストを返して終了。 replace(New, Old, [Old|T]) % ② head が旧要素に一致する場合 -> [New|replace(New, Old, T)]; % head を新要素に置き換え、tail に対して再帰呼び出し。 replace(New, Old, [H|T]) % ③上記以外の場合 -> [H|replace(New, Old, T)]. % head はそのままで tail に対して再帰呼び出し。 |
(注:この replace は、あくまでも練習のための例です。本気で使ったら stack overflow になるかもしれません。"末尾再帰最適化" で検索!)
実行してみましょう。
1 2 3 4 5 6 7 |
$ erl 1> c(f_test). {ok,f_test}. 2> f_test:replace(foo, y, [x, y, z, z, y]). [x,foo,z,z,foo] |
上記 ② においては、引数を受け取る段階で、リストを head と tail に分解し、head が旧要素と一致するかどうかの検査までを行っているわけです。
このような仕組みのおかげで、慣れればループなんかなくても気にならなくなります(きっと)。関数型プログラミングの勉強のために Erlang を選択するというのも悪くないかもしれません。
とはいえ、Erlang と言えば、やはりアクターモデルによる並列処理でしょう。
アクターモデル
複数のスレッドを起動して並列に処理を行うと競合が発生します。どうしますか?
1 つの解決策は、ロックを取得し、ロックを取得したスレッド以外には待機させる、という方法です。Java の synchronized 文とかですね。
また、「競合が発生するのはスレッドが処理の途中で勝手に割り込むからだ。だから割り込むのやめよう」というのが別の方法で、これがファイバとかコルーチンとか呼ばれてるやつです。
これらに対し、「いや、そもそもメモリを共有しなければ競合なんてありえないでしょう」というのが Erlang の考え方です。アクターと呼ばれる Erlang の軽量プロセスは、プロセス間でメモリを共有せず、互いにメッセージ(数値でもリストでも何でも良い)を送りあって並列処理を行います。これがアクターモデルと呼ばれています。
メッセージの送信は、演算子 !(感嘆符) を使って、
プロセスID ! メッセージ
で行います
まず ! の動作確認のため、Erlang シェルのプロセスにメッセージを送ってみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ erl 1> self(). % 自身(つまり Erlang シェル)のプロセスIDを返す <0.32.0> 2> self() ! foo. % メッセージを送信 foo 3> self() ! [bar, baz]. % もう一つメッセージ(今度はリスト)を送信 [bar,baz] 4> flush(). % 受け取ったメッセージを確認 Shell got foo Shell got [bar,baz] ok |
では、以下の仕様の box プロセスを作成し、このプロセスと Erlang シェルのプロセスの間で通信してみましょう。
[box プロセスの仕様]
・リストを保持する。
・メッセージ {送信元プロセスID, {put, アイテム}} を受け取ると、リストにアイテムを追加し、追加した旨の文字列を送信元プロセスに送る
・メッセージ {送信元プロセスID, show_list} を受け取ると、現在のリストを送信元プロセスに送る
メッセージの受信は receive で行います。
● am_test.erl
1 2 3 4 5 6 7 8 9 10 11 12 |
-module(am_test). -compile(export_all). box(ItemList) -> receive {Pid, {put, Item}} -> % ①{プロセスID, {put, アイテム}} の場合 Pid ! lists:concat(["You put ", Item]), % 送信元プロセスに文字列を送り、 box([Item|ItemList]); % リストにアイテムを加えて再帰呼び出し。 {Pid, show_list} -> % ②{プロセスID, show_list} の場合 Pid ! ItemList, % 送信元プロセスにアイテムのリストを送り、 box(ItemList) % リストはそのままで再帰呼び出し。 end. |
プロセスの起動は関数 spawn で行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$ erl 1> c(am_test). {ok,am_test}. 2> BoxPid = spawn(am_test, box, [[dog]]). % box プロセス起動(リストに dog を入れておく)。 <0.40.0> 3> BoxPid ! {self(), {put, cat}}. % cat を追加するメッセージを送信。 {<0.32.0>,{put,cat}} 4> BoxPid ! {self(), {put, rabbit}}. % rabbit を追加するメッセージを送信。 {<0.32.0>,{put,rabbit}} 5> BoxPid ! {self(), show_list}. % リストを返させるメッセージを送信。 {<0.32.0>,show_list} 6> flush(). % box から Erlang シェルへの返信を全て表示。 Shell got "You put cat" Shell got "You put rabbit" Shell got [rabbit,cat,dog] ok |
簡単な例ですが、ちゃんとプロセス間でメッセージの送受信ができましたね。
Erlang をはじめてみた感想
Erlang の独特の文法は、趣味で勉強する分にはとても面白いのですが、これで業務アプリケーションを書くとなると、やはり、まだちょっと抵抗があります。それでも、アクターモデルによる並列処理は、頑張って習得する価値があると感じました。
私たちの Erlang はこれからだ!
(筆者の次回作に御期待下さい。)
参考文献
Hébert,Fred (2014)『すごいErlangゆかいに学ぼう!』(山口能迪訳) オーム社.
この本は、もともと Web で公開されていた記事が書籍化されたもので、Web 版は今でも無料で閲覧できます。
原文:http://learnyousomeerlang.com/content
翻訳:http://www.ymotongpoo.com/works/lyse-ja/index.html