ErlangとJavaをつなぐ
この記事では、ErlangとJavaの連携方法を説明します。
使い道
Erlangで出来ているプログラムを使いたいが、今Erlangが分かる人がいない。
でも、Javaが分かる人はいる。こういった状況でErlang + Javaが使えるのではないでしょうか。
また、Erlangがネットワークや並行処理に強い反面、
他の言語に比べてパフォーマンスがよくないところ(演算など)があるようで、
そういった部分はJavaに担当させるのもいいでしょう。
GUIやRDB操作はJavaに任せて、分散処理はErlangに担当させるなどもありだと思います。
実際、Erlang + Javaではありませんが、
ある商用のメール配信エンジン(?)は、
メインをErlangにして、計算速度が必要な部分は
C言語で開発したとの記事を見たことがあります。
Jinterface
JavaからErlang プログラムを呼び出すための、Erlang 公式Java APIパッケージです。
APIは実際通信を行う部分とErlangデータタイプをJavaクラスとして実装した部分で構成されています。
このAPIパッケージを使えば、Erlangプログラムと連携できます。
jinterfaceのjarファイルは、${ERLANG_HOME}/lib/jinterface(又はjinterface-x.y.z)/privディレクトリ配下にOtpErlang.jarという名前で配置されています。
サンプル
受け取ったメッセージをそのまま返すエコサーバのようなErlangプログラムと、
そのエコサーバを呼び出すJavaプログラムを作成してみます。
まず、Erlangプログラムです。
Erlang 基礎ポイント7 - 分散処理 - DukeLabに出たサンプルソースと同じです。
-module(echo). -export([start/0, send/2]). start() -> register(echo, spawn(fun() -> loop() end)). send(Msg, Receiver) -> Receiver ! {self(), {server, Msg}}, receive {client, Res} -> Res end. loop() -> receive {From, {server, Msg}} -> From ! {client, string:concat("Echo response : ", Msg)}, loop() end.
このErlangプログラムをコンパイルして、実行しておきます。
今回は、VirtualBox上に動くUbuntu 12.04で実行しました。
REPL環境上でテストもしてみます。
Javaからアクセスする時、識別に必要なため、erl起動時にnameとsetcookieの指定が必要です。
nameとsetcookieはメモしておいて下さい。
$ erl -name echoserver@vpc1.com -setcookie echocookie (echoserver@vpc1.com)1> c(echo). {ok,echo} (echoserver@vpc1.com)2> echo:start(). true (echoserver@vpc1.com)3> echo:send("Hello!!", echo). % 自分自身への呼出でテスト。 "Echo response : Hello!!"
次は、Javaプログラムです。
ビルドパスにjinterfaceのjarファイル(OtpErlang.jar)を追加して下さい。
今回は、Windows 7環境とEclipseで作成しました。
こちら側のPCにもErlangがインストールされていなければなりません。
jinterfaceのjarだけでは動かないので、注意して下さい。
package dukelab.erlangjavatest; import java.io.IOException; import com.ericsson.otp.erlang.OtpErlangAtom; import com.ericsson.otp.erlang.OtpErlangDecodeException; import com.ericsson.otp.erlang.OtpErlangExit; import com.ericsson.otp.erlang.OtpErlangObject; import com.ericsson.otp.erlang.OtpErlangString; import com.ericsson.otp.erlang.OtpErlangTuple; import com.ericsson.otp.erlang.OtpMbox; import com.ericsson.otp.erlang.OtpNode; public class ErlangJavaTest { public static void main(String[] args) throws IOException, OtpErlangDecodeException, OtpErlangExit { // javanode : Javaプログラム側のホスト名です。任意で指定します。 // echocookie : Erlangプログラム側でerl実行時、指定したクッキー名と同じです。 OtpNode node = new OtpNode("javanode", "echocookie"); OtpMbox mBox = node.createMbox(); // メッセージをErlangプログラム側に送ります。 // これをErlangプログラムで表現すると、 // { echo, 'echoserver@vpc1.com' } ! { self(), {server, "Hello Erlang, I am Java."} }になります。 { OtpErlangTuple tuple = createTuple( mBox.self(), createTuple( new OtpErlangAtom("server"), new OtpErlangString("Hello Erlang, I am Java.") ) ); // echo : Erlangプログラム側で登録したプロセス名です(register(echo, spawn(fun() -> loop() end)))。 // echoserver@vpc1.com : Erlangプログラム側でerl実行時、指定したホスト名です。 mBox.send("echo", "echoserver@vpc1.com", tuple); } // 応答を受け取ります。 { OtpErlangObject result = mBox.receive(); OtpErlangTuple tuple = (OtpErlangTuple) result; OtpErlangAtom atom = (OtpErlangAtom) tuple.elementAt(0); OtpErlangString str = (OtpErlangString) tuple.elementAt(1); System.out.println("Response from Erlang : " + str); } } private static OtpErlangTuple createTuple(OtpErlangObject... msg) { return new OtpErlangTuple(msg); } }
その後、JavaプログラムのPCにあるhostsファイルに、
先ほどのErlang実行時にnameに指定したドメイン名とIPアドレス(Erlangを実行したPC)の対応を追加します。
例えば、以下のようにです。
192.168.56.101 vpc1.com
さあ、Javaプログラムを実行してみましょう。
Response from Erlang : "Echo response : Hello Erlang, I am Java."
できました!
整理
jinterfaceを使うと、Erlangプログラムと同じく動作するプログラムを
Javaで作成することができます。
サーバ役割のプログラムもクライアント役割のプログラムもできます。
いくつかのErlang機能に対応するJavaクラスを以下に並べます。
- Erlang起動(Erlangの仮想マシン) : OtpNode
- メッセージをやりとりする(Erlangでの!やreceiveなど) : OtpMbox
- タプル : OtpErlangTuple
- 文字列 : OtpErlangString
- アトム : OtpErlangAtom
この記事が理解できたら、後は、参考資料を読みながら、掘り下げていけばいいと思います。
Erlang 基礎ポイント5 - 並列処理2
今日はErlangの並行処理における例外処理について勉強したいと思います。
並行処理での例外処理
Erlang 基礎ポイント4で並行処理プログラムの作成が可能になりました。
しかし、現在実行中のErlangプロセス(=Pといいます)から子Erlangプロセス(=Cといいます)を起動し、
そのCからエラーが発生した時の対処方法はまだ勉強していません。
これができないと、Erlangプロセス全体が一つの塊として、機能するのは難しいですね。
まるで細胞同士が協力し、一つの大きい生き物が成り立つのに似ていると思います。
リンク
PからCを生成し、PとCは何の関係もないならば、それまです。
こういう場合、PやCに何かエラーが起こってもお互いは何の関係も影響もありません。
しかし、Cにエラーが起きて、Pも一緒にエラーを起こすか、エラーを修復する処理を
したい場合、PとCの間にリンクを作らなければなりません。
リンクは言葉通りにPとCが繋がっているのを意味し、
一方のErlangプロセスに何か起きた場合、
他方のプロセスにそれが伝わり、他方が自分も終了させたり他の処理をしたい時、使われます。
普通のErlangプロセスを生成する時は、spawn()関数を使いますが、
リンク付きのErlangプロセスを生成したい場合、spawn_link()関数を使います。
以下のサンプルでspawn()とspawn_link()を比較してみましょう。
生成した子Erlangプロセスでエラーが起きた時の動作の違いが分かります。
まず、spawnを使うサンプルです。
% spawn_sub.erlファイルです。 % spawn_test.erlとspawn_link_test.erlで共通で使うための関数を持っています。 -module(spawn_sub). -export([loop/1]). % 2秒おきにメッセージ出力 loop(Msg) -> receive % 終了処理 exit -> exit; % わざと、0で割ってエラーを起こす。 % コンパイル時、警告が出ますが、学習用なので、無視して下さい。 error -> 1 / 0 after 2000 -> io:format("Process ~p ~n", [Msg]), loop(Msg) end. % spawn_test.erlファイルです。 -module(spawn_test). -import(spawn_sub, [loop/1]). -export([spawn_test/0]). % テストメソッド spawn_test() -> spawn(fun p/0). % 親Erlangプロセス p() -> CPid = spawn(fun c/0), % 後でchildと言う名前で子Erlangプロセスにerrorメッセージを送れるようになる。 register(child, CPid), loop("Parent"). % 子Erlangプロセス c() -> loop("Child"). % REPL環境で試したものです。 1> c(spawn_sub). ./spawn_sub.erl:15: Warning: this expression will fail with a 'badarith' exception {ok,spawn_sub} 2> c(spawn_test). {ok,spawn_test} 3> spawn_test:spawn_test(). <0.44.0> Process "Child" Process "Parent" Process "Parent" Process "Child" Process "Child" Process "Parent" 4> child ! error.% 子Erlangプロセスにエラーを起こします。 =ERROR REPORT==== 24-Jun-2014::10:12:28 === Error in process <0.45.0> with exit value: {badarith,[{spawn_sub,loop,1}]} error Process "Parent" % Childが出力されなくなりました。しかし、Parentは出力し続けています。 Process "Parent" Process "Parent"
次はspawn_linkを使うサンプルです。
% spawn_link_test.erlファイルです。 -module(spawn_link_test). -import(spawn_sub, [loop/1]). -export([spawn_link_test/0]). spawn_link_test() -> spawn(fun p/0). p() -> CPid = spawn_link(fun c/0), % ここだけがspawn_testと違います。 register(child, CPid), loop("Parent"). c() -> loop("Child"). % REPL環境で試したものです。 1> c(spawn_sub). ./spawn_sub.erl:15: Warning: this expression will fail with a 'badarith' exception {ok,spawn_sub} 2> c(spawn_link_test). {ok,spawn_link_test} 3> spawn_link_test:spawn_link_test(). <0.44.0> Process "Child" Process "Parent" Process "Parent" Process "Child" Process "Child" Process "Parent" Process "Parent" Process "Child" 4> child ! error. % 子Erlangプロセスにエラーを起こします。 =ERROR REPORT==== 28-May-2014::13:41:59 === % 親Erlangプロセスも子Erlangプロセスも終了してしまいました。 Error in process <0.45.0> with exit value: {badarith,[{spawn_sub,loop,1}]} error 5>
spawnの方はCが死んでも、Pは動き続けていることが分かります。
つまり、CがどうなってもPは構いません。
なんというか、単細胞がただ分裂するような感じですね。
Javaでたとえると、単純にスレッドを起こしてそれでおしまいという感じです。
逆にspawn_linkの方はCが死んだら、Pも死ぬことが分かります。
プロセスが全部死ぬので、実際のシステムだったら、システムの停止を意味するので、まずいでしょうが、
spawnの代わりにspawn_linkを使うだけで、複数のErlangプロセスがひとかたまりとして、
動く(... 死ぬ)ようになる点はすごいと思います。
Javaでこれをやるためには親スレッドと子スレッドが通信するための何らかの
仕組み(フラグ変数、ブロッキングキュー、Latch等)が必要になってくるでしょう。
システムプロセス
spawn_linkを使って、PとCの間にリンクが置けることが分かりました。
しかし、それだけでは、Cにエラーが起きて死んだ場合、Pも死ぬだけです。
Cが死んで、Pも死んで、PのPも死んで... 結果的にシステムが停止...
これでは実用性がないですし、耐久性があるシステムとはいえないですね。
システムのどこかにエラーが起きた場合、それを捕らえて、修復したり、一部の機能を停止したりして、
システムが動き続けるようにするのが理想的でしょう。
Cにエラーが起きて、Cが死んだ場合、Pも一緒に死ぬのではなく、
PがCのエラーを捕らえて何かをしたい場合、Pをシステムプロセスに指定する必要があります。
Pをシステムプロセスにすると、Cのエラーを捕らえて何かをすることができます。
以下のサンプルを見てみましょう。
% system_process_test.erlファイルです。 -module(system_process_test). -export([test/0]). test() -> spawn(fun p/0). p() -> % システムプロセスに変身します。 process_flag(trap_exit, true), CPid = spawn_link(fun c/0), register(child, CPid), loop("Parent"). c() -> loop("Child"). loop(Msg) -> receive {'EXIT', Pid, Reason} -> io:format("We catched error(Pid:~p, Reason:~p)~n", [Pid, Reason]), loop(Msg); exit -> exit; error -> 1 / 0 after 2000 -> io:format("Process ~p ~n", [Msg]), loop(Msg) end. % REPL環境で試したものです。 2> c(system_process_test). ./system_process_test.erl:25: Warning: this expression will fail with a 'badarith' exception {ok,system_process_test} 3> system_process_test:test(). <0.43.0> Process "Child" Process "Parent" Process "Parent" 、 Process "Child" Process "Child" Process "Parent" 4> child ! error. % 子Erlangプロセスにエラーを起こします。 =ERROR REPORT==== 28-May-2014::13:55:31 === % エラーは発生しますが... Error in process <0.44.0> with exit value: {badarith,[{system_process_test,loop,1}]} We catched error(Pid:<0.44.0>, Reason:{badarith, % PでCのエラーをちゃんと捕らえています。 [{system_process_test,loop,1}]}) error Process "Parent" % エラーを捕らえた後もPは動きつづけています。 Process "Parent" Process "Parent" Process "Parent"
システムプロセスにしたい場合、process_flag(trap_exit, true)を呼び出せばいいです。
trueの代わりにfalseにすると、普通のErlangプロセスになりますが、
falseに指定することはあまりないと思います。
そもそもprocess_flag自体を呼び出さなければ、普通のErlangプロセスになるからです。
明示的なリンクの作成
spawn_linkでなく、spawnでErlangプロセスを作成しましたが、
後で親Erlangプロセスと子Erlangプロセスの間に
リンクを作成したい場合、link関数を使えばいいです。
以下のサンプルを参考して下さい。
% link_test.erlファイルです。 -module(link_test). -import(spawn_sub, [loop/1]). -export([test/0]). test() -> spawn(fun p/0). p() -> CPid = spawn(fun c/0), link(CPid), % 明示的にリンクを作成します。 register(child, CPid), loop("Parent"). c() -> loop("Child"). % REPL環境で試したものです。 1> c(link_test). {ok,link_test} 2> c(spawn_sub). ./spawn_sub.erl:15: Warning: this expression will fail with a 'badarith' exception {ok,spawn_sub} 3> link_test:test(). <0.44.0> Process "Child" Process "Parent" Process "Parent" Process "Child" Process "Child" Process "Parent" Process "Parent" Process "Child" 4> child ! error. % 子Erlangプロセスにエラーを起こします。 =ERROR REPORT==== 28-May-2014::14:01:20 === % 親Erlangプロセスも子Erlangプロセスも止まりました。 Error in process <0.45.0> with exit value: {badarith,[{spawn_sub,loop,1}]} error 5>
注意すべきは一見、spawn_linkはspawn + linkで代替できそうですが、実はそうではありません。
spawn_linkはErlangプロセスの生成とリンクの作成が原子的に実行されますが、
spawn + linkはErlangプロセスの生成(spawn)とリンクの作成(link)の間に隙があるので、
原子的に動くのではないからです。
spawnでErlangプロセスを生成した後、linkが実行される前のほんの少しの間に
生成したErlangプロセスが死ぬことがありえます。
今日はここまでにします。
ここまで勉強すると、ある程度、Erlangで並行処理プログラムを作れるのではないでしょうか。
Erlangは並行処理プログラムの作成がとても簡単ですね。
次は並行処理の続きでモニタについて勉強したいと思います。
参考書

- 作者: Joe Armstrong,榊原一矢
- 出版社/メーカー: オーム社
- 発売日: 2008/02/23
- メディア: 単行本(ソフトカバー)
- 購入: 8人 クリック: 284回
- この商品を含むブログ (97件) を見る
Erlang 基礎ポイント4 - 並行処理1
今日はErlangで並行処理プログラムを作成する方法を勉強します。
並行処理
並行処理とは、一つのプログラムの中で複数の処理を同時に実行させることです。
Windows、LinuxなどのOSでは、複数のプログラムが複数のプロセスとして同時に実行されます。
Apache TomcatなどのAPサーバの内部では、複数の処理単位(リクエスト、サーバ)が複数のスレッドとして同時に実行されます。
Erlangでは、複数の処理が複数のプロセスとして実行されます。
しかし、名前はプロセスですが、ErlangのプロセスはOSのプロセスとは違います。
あくまでErlang内に存在するプロセスなのです。
これからErlangの並行処理に登場するプロセスは、Erlangプロセスと意識するようにしましょう。
基本
Erlangで作成する並行処理プログラムはJavaなどの言語で記述するスレッド基盤のプログラムとは記述方法が違います。
が、プログラム(又はメインスレッド・メインプロセス)の実行中にいきなり並行に何か(Erlangプロセス)を起動するということは同じです。
Erlangで並行処理プログラムを書くのはとても簡単ですが、それでも掘り下げると色々出てきます。
ここでは最小限の説明だけをします。後はマニュアルや他のウェブサイトを参考して、知識を拡張していけばいいと思います。
まずは、サンプルプログラムを分析し、その後、並行処理プログラムの作成方法を整理してみましょう。
% concurrent.erl -module(concurrent). -export([start/0]). start() -> % A spawn(fun() -> echo(1) end). echo(NoMsgCnt) -> % B receive shutdown -> io:format("Shutdown echo~n"); {one, Msg} -> io:format("One Msg ~p~n", [Msg]), echo(NoMsgCnt); {two, Msg} -> io:format("Two Msg ~p~n", [Msg]), echo(NoMsgCnt); Unknown -> io:format("Unknown Msg ~p~n", [Unknown]), echo(NoMsgCnt) after % C 7000 -> io:format("No Msg ~p~n",[NoMsgCnt]), echo(NoMsgCnt + 1) end.
まず、start関数内のコメントAを見てみましょう。
spawn関数が見えますね。
spawn関数を呼び出すと、引数に指定した関数が処理内容として新しいErlangプロセスが生成及び実行されます。
新しいErlangプロセスが生成されると、spawn関数を実行したErlangプロセスと新しいErlangプロセスが同時、つまり、並行で
実行されるようになります。
spawn関数はErlangプロセスのID(PID)を返します。PIDは後ほど、Erlangプロセスと通信する時に必要です。
spawn関数に指定した関数の内部ではecho関数を呼び出しています。
echo関数の内部を見てみましょう。新しいErlangの文法が出てきました。
receiveとafterですね。
基本的にspawn関数に指定する関数の内容は何でもいいです。
非同期で何かの処理を分散して行うように、新しいErlangプロセスを実行した後、そのErlangプロセスを忘れてもいい場面もあるでしょうが、
自分を実行した親Erlangプロセスはもちろんのこと、他のErlangプロセスと通信する必要がある場面がもっと多いと思います。
プロセス間の通信のため、Javaでしたら、同期しながら、変数を値を調べるとか...色々と頭が痛くなりますが、
(concurrentパッケージにあるクラスを使えば大体解決できますが、Erlangよりは確かに冗長です)
Erlangでは非常に単純にErlangプロセス間の通信が実現できます。
新しいErlangプロセスが立ち上がると、そのErlangプロセスのためのメールボックスが用意されます。
形こそ、hotmailやgmailのようなメールとは違いますが、概念的には同じです。
receive文が実行されると、メールボックスにメッセージが入ってくるまで待機(ブロッキング)状態になります。
メッセージが同時に多数届いた場合、それぞれのメッセージが非同期で処理され、処理が終わったメッセージは削除されます。
メッセージはreceive内のパターンと照合され、マッチしたパターンの処理内容が実行されます。
もし、どのパターンにもマッチしなかった場合、何も実行されません。
afterには一定の時間がすぎるまでメッセージが届いていない場合、つまり、タイムアウトした場合、
処理される内容が置かれます。単位はmsです。
上記には7000とあるので、7秒間、メッセージが届かない場合、No Msg Xと表示されます。
receive内の各パターン及びafterの処理内容を見ると、処理内容の最後に再帰呼出(echo関数の呼出)をしているのが分かります。
これはまた次のメッセージを待つためです。再帰呼出をしないと、そのままErlangプロセスが終了してしまいますので、ご注意下さい。
shutdownパターンには再帰呼出がないことから、Erlangプロセスを終了するということが分かります。
パターンにあるアトムがshutdownですから、意図的ですね。
これでErlangで並行処理を記述することができました。
次は並行処理を実行し、Erlangプロセスと通信する方法を勉強しましょう。
以下は上記のconcurrentモジュールをREPL環境でテストしたものです。
1> c(concurrent). {ok,concurrent} 2> P = concurrent:start(). <0.39.0> No Msg 1 3> P ! {one, "Hello"}. One Msg "Hello" {one,"Hello"} 4> P ! {two, "Hello"}. Two Msg "Hello" {two,"Hello"} 5> P ! {three, "Hello"}. Unknown Msg {three,"Hello"} {three,"Hello"} No Msg 2 No Msg 3 6> P ! shutdown. Shutdown echo shutdown 7>
concurrentモジュールをコンパイルし、start関数を呼び出しました。
戻り値のPIDを変数Pに代入しました。
PはPIDですが、Erlangプロセスへの電話だと考えればいいです。
Pへの連絡はどうすればいいか? ビックリマーク(!)を使えばいいです。
PID ! メッセージ
です。これがErlangプロセスにメッセージを送る方法です。簡単でしょう?
これでメッセージが該当PIDのErlangプロセスに送られると、
メッセージとErlangプロセスのreceive内にあるパターンとの照合が行われ、
マッチしたパターンの処理内容が実行されます。
整理してみましょう。
% 新しいErlangプロセスの生成は PID = spawn(fun() -> xxx(引数...) end) % Erlangプロセスでメッセージを受け取るには xxx(引数...) -> receive パターン1 -> 処理1, ... 処理x, xxx(引数...); % xxx()を再帰呼出しないと、終了してしまう。 ... パターンx -> 処理1, ... 処理x, xxx(引数...) % 最後のパターンにはセミコロンを書かない after % afterは省略可能 タイムアウト値(ms) -> 処理 xxx(引数...) end. % Erlangプロセスと通信するためには PID ! メッセージ
さらに
これで、Erlangで動く並行処理プログラムを書けるようになりましたが、
ちょっと足りない部分があります。
それぞれのErlangプロセスとやり取りするため、メッセージを送る時にspawn関数から返されるPidが必要ですが、
Pidをいちいち覚えておくのは面倒です。
そこで、ErlangにはPidを意味ある名前で登録できるようにする関数が用意されています。
その関数でPidを名前に登録しておき、後でその名前でErlangプロセスにメッセージを送ればいいです。
使い方は簡単です。以下のようにすればいいです。
register(名前を表すアトム, Pid)
反対に登録した名前を解除する関数もあります。
unregister(名前を表すアトム)
簡単ですね。
以下は基本で示したサンプルプログラムをregisterを使うように修正し、REPL環境で試したものです。
% concurrent2.erl -module(concurrent2). -export([start/0]). start() -> P = spawn(fun() -> echo(1) end), register(test_echo, P). echo(NoMsgCnt) -> receive shutdown -> io:format("Shutdown echo~n"); {one, Msg} -> io:format("One Msg ~p~n", [Msg]), echo(NoMsgCnt); {two, Msg} -> io:format("Two Msg ~p~n", [Msg]), echo(NoMsgCnt); Unknown -> io:format("Unknown Msg ~p~n", [Unknown]), echo(NoMsgCnt) after 7000 -> io:format("No Msg ~p~n",[NoMsgCnt]), echo(NoMsgCnt + 1) end. % REPL環境でテスト。 1> c(concurrent2). {ok,concurrent2} 2> concurrent2:start(). true No Msg 1 No Msg 2 No Msg 3 3> test_echo ! {one, "Hello"}. One Msg "Hello" {one,"Hello"} 4> test_echo ! {two, "Hello"}. Two Msg "Hello" {two,"Hello"} 5> test_echo ! {three, "Hello"}. Unknown Msg {three,"Hello"} {three,"Hello"} No Msg 4 6> test_echo ! shutdown. Shutdown echo shutdown 7>
PIDでなく、test_echoという分かりやすい名前でErlangプロセスと通信できていますね。
Erlangプロセスは他の言語のスレッドやOSのプロセスより早くて効率がいいです。
数万・数十万個のプロセスも問題なく処理できるそうだから、すごいですね。
自分でErlangの威力が直接感じられるプログラムが書けたらいいですね。
アイデアが浮かんだら、ぜひやってみたいと思います。
ちなみにErlangでネットワークプログラムを作ると、NodeJSと比べてどうでしょうか。
メリットが少し似ているようが気がします。ググれば出るのかな...
今日はここまでにします。
元々今日でErlang 基礎ポイントを最後にしようと思いましたが、
並行処理での例外処理について意外と書くことが多くて、
次の記事に続けて書くことにします。
最後になるかどうかは分かりません。これはあくまで個人的な理由で書くものですから...笑
Erlang 基礎ポイント3 - case式, if式 例外
case式
Erlang 基礎ポイント2 - DukeLabでパターン集合について説明しました。
パターン集合の結果から処理を複数の関数に分けることができましたが、
関数の数が無駄に多くなる恐れがありますね。
もちろん、プログラムの処理は可能な限り小さくした方が
モジュール化側面でいいでしょうが、やりすぎると、
かえって複雑なプログラムになりかねないですね。
case式を使うと、一つの関数内で処理部を分けておくことができます。
Javaのswitch caseのようなものですが、判断条件に固定値だけでなく、パターンが使えるので、
もっと強力です。
まず、例をみてみましょう。
前のguard_module.erlがErlang 基礎ポイント2 - DukeLabのパターン集合節で紹介したもので
後のcase_test.erlがcase式を使うように修正したものです。
% guard_module.erl -module(guard_module). -export([guard_test/1]). guard_test(X) when X < 5, X > 1 -> X * X; guard_test(X) when X >= 5; X < 10 -> X - 2; guard_test(X) -> X - 2. % case_test.erl -module(case_test). -export([guard_test/1]). guard_test(X) -> case X of X when X < 5, X > 1 -> X * X; X when X >= 5; X < 10 -> X - 2; X -> X - 2 end.
caseの使い方は次のようです。
他のプログラミングの経験があるなら、問題ないでしょう。
case 評価対象 of パターン1 [when ガード...] -> 結果; ... パターンn [when ガード...] -> 結果 end
最後のパターンにはセミコロンをつけないことに注意して下さい。
評価対象に判断対象(関数引数・変数など)を指定し、
パターンnに判断対象と照合するパターンを書きます。
必要ならガードに追加条件を指定できますが、ガードは省略可能です。
評価対象がパターンに一致すれば、結果が返されます。
一致するパターンがないと、例外が発生するので、注意しましょう。
caseの方が関数の長さが増えましたが、関数の数は減りましたね。
どちらも優先的に使うのではなく、関数の数とcaseの数をバランスよく
配置すればいいと思います。
if式
他のプログラミング言語のif文と同じですが、違いは式なので、戻り値があるということです。
上記のcase式のサンプルをif式に修正したものを載せます。
% if_test.erl -module(if_test). -export([if_test/1]). if_test(X) -> if X < 5, X > 1 -> X * X; X >= 5, X < 10 -> X - 2; true -> X - 3; end. % if_testをREPL環境で試す。 19> c(if_test). {ok,if_test} 20> if_test:if_test(7). 5 21> c(if_test). {ok,if_test} 22> if_test:if_test(3). 9 23> if_test:if_test(6). 4 24> if_test:if_test(-1). -4
if式の文法は他のプログラミング言語のif文と似ていますが、ちょっと違いますね。
まず、複数個の条件を書く時、ifと複数書く必要がありません。
最初にifと書いて、case式のように条件と結果を次々と書いていけばいいです。
それから、最後の条件にtrueと書きましたね。これはelseと考えればいいです。
Erlangのifにはelseがありません。なので、全ての条件に一致しない時の条件を
直接書く必要がありますが、trueと書いておけばOKです。
case 評価対象 -> パターン1 [when ガード...] -> 結果; ... パターンn [when ガード...] -> 結果 end
trueを書かないで、全ての条件に一致しない場合、例外が発生しますので、注意しましょう。
例外
Erlangで例外処理は、ちょっと形式は違いますが、Javaと同じくtry-catch式を使います。
以下の例を見ながら、try-catchを身につけましょう。
% trycatch.erl -module(trycatch). -export([go/1]). go(X) -> try test(X) of Val -> {value, Val} catch throw:exception1 -> io:format("Catch Exception_Test1~n"); throw:exception2 -> io:format("Catch Exception_Test2~n"); exit:_ -> io:format("Catch Exit~n"); error:_ -> io:format("Catch Error~n") after io:format("After~n") end . test(X) -> case X of 1 -> throw(exception1); 2 -> throw(exception2); 3 -> exit(unknown); 4 -> error(unknown) end . % trycatchをREPL環境で試す。 1> c(trycatch). {ok,trycatch} 2> trycatch:go(1). Catch Exception_Test1 After ok 3> trycatch:go(2). Catch Exception_Test2 After ok 4> trycatch:go(3). Catch Exit After ok 5> trycatch:go(4). Catch Error After ok
関数goに1から4までの数字を入れると、例外(throw)2種類、プロセス終了(exit)1種類、エラー(error)1種類を演じてくれます。
throwは呼出元が例外を処理することを期待する時、呼出元に対してエラーをなげる関数です。JavaのChecked Exceptionと似ていますね。
但し、throwでエラーを発生させても、呼出元がtry-catchでエラーを捉えなくても実行はできます。
Javaのようにコンパイルエラーは発生しません。
プログラムで捉えられなかったエラーはErlangが捉えます。その場合、スタックトレースのようなものが表示されるでしょう。
exitはプロセスを終了させたい時、呼出元にエラーをなげる関数です。try-catchでこのエラーを捉えないと、プロセスが終了してしまいます。
errorは呼出元が例外を処理できないような致命的なエラーが発生したことを知らせるため関数です。
Javaでいうと、RuntimeExceptionのようなものでしょうか。
よく見るとErlangのtry-catchはcaseと似ていることが分かります。
tryとcatchの間のコードがまさにcaseのような形式、その後、catchとafterが追加されていますね。
catchはJavaのcatchと同じで、afterはJavaのfinallyのようなものです。
以下はtry-catch式の使い方です。
try 評価対象 of パターン1 [when ガード...] -> 結果; ... パターンn [when ガード...] -> 結果 catch 例外種類1(throw, exit, error): 例外パターン1 [when ガード1] -> 結果1; ... 例外種類n(throw, exit, error): 例外パターンn [when ガードn] -> 結果n after catch結果に関係なく、返す結果 end
Erlangのtry-catchはJavaと違って、式なので戻り値があります。
今日はここまで。
次はErlangでの並行処理について書きます。
Erlang 基礎ポイント2 - プログラムの形式, 無名関数, パターン照合など
Erlangプログラムの形式
REPL環境でなく、ソースファイル(erlファイル)から実行するためには
以下の形式に従う。
-module(test). -export([func1/0,func3/1,func4/3]). func1() -> func2() + 5. func2() -> 3. func3(X) -> Y = X * 2, Y * 2. func4(plus, X, Y) -> X + Y; func4(minus, X, Y) -> X - Y; func4(_, _, _) -> "nothing". % コメント
まず、module文にモジュール名を書く。
モジュール名はソースコードを含んでいるファイル名(拡張子はerl)にする。
モジュールはErlangコードの基本単位だ。
export文には外部からアクセスを許容する関数名・引数の数を記述する。
Javaにおけるpublicといえば、分かりやすい。
コンマで区切って複数指定可能。
次はJavaのようにメンバー変数が来そうだが、そんなものはない。
すぐ処理ロジックを含む関数が来る。
関数のシグネチャにリターンタイプとか、引数の型の指定がない。
関数名と引数名だけを書いて、->の後、すぐ処理内容を記述する。
関数の処理内容は行ごとにコンマで区切って、関数の最後にピリオドを打つ。
引数の数が同じ関数を複数(いわゆる、オーバーロード)書く場合、
各関数の終わりにコンマでなくセミコロンを書く。
但し、最後の関数の終わりにはコンマを書く。
型のチェックは実行時に行われる。
つまり、Erlangは動的型付け言語である。
コメントは%の後に書く。
無名関数
Erlangでは関数を無名で宣言して、変数に代入したり関数の引数や戻り値として使用することができる。
1> F = fun(A) -> A * A end. #Fun<erl_eval.6.80247286> 2> F(2). 4 3> Y = F. #Fun<erl_eval.6.80247286> 4> Y(4). 16
funで始まって、end.(ピリオドがあることに注意)で終わる。
次は無名関数を関数の引数に渡したり、戻り値として返す例だ。
% 引数 9> F = fun(A) -> A * A end. #Fun<erl_eval.6.80247286> 10> lists:map(F, [1, 2, 3, 4]). [1,4,9,16] % 戻り値 12> Plus = fun(X) -> (fun(Y) -> X + Y end) end. #Fun<erl_eval.6.80247286> 13> PlusFive = Plus(5). #Fun<erl_eval.6.80247286> 14> PlusFive(9). 14
リスト内包表記
リストから特定条件を満たす要素で構成された新しいリストを作るためのErlangの機能。
普通、リストから特定要素を持つ新しいリストを作るためには
新しい空のリストを宣言しておいて、ループと条件文を利用し、
条件にマッチした要素を空のリストに入れる処理が必要なので、
コードが長くて読みづらくなる。
しかし、リスト内包表記を使うと、リスト(例 : [ 1, 2, 3])の中で
新しいリストを作るための専用の表記ができるので、コードが短くて
読みやすい。
1> L = [1, 2, 3]. [1, 2, 3] 2> [X - 1 || X <- L]. [0, 1, 2].
後ろの部分、X <- LはリストLから要素Xを取り出すという意味で、
"||"の左側に要素Xで行う式を書く。
レコード
Cにおける構造体のようなもの。
Erlangでレコードの宣言方法は色々あるが、よく使われる方法は
拡張子がhrlのファイルにレコードを定義し、
そのhrlファイルをerlファイルで
インクルードする方法だ。
% person.hrl -record(person, { name, age, tel = 000-0000-0000 }).
% test.erl -module(test). -export([duke/0]). -include("person.hrl"). #インクルード duke() -> #person{name=duke, age=34, tel="0800000-0000"}.
レコードの宣言は、まず、-recordキーワードを書いて、
1番目の引数にレコード名、次に要素名のタプルを指定する。
要素にはデフォルト値の指定ができる。
レコードの使用は
#レコード名{要素1=値, ... , 要素x=値}
のようにする。
パターン照合
Erlang 基礎ポイント1 - DukeLabでちょっとだけ、触れたが、ここでもうちょっと説明しよう。
Erlangで、=は代入するのではなく、パターン照合するという。
以下の場合、Aに100を代入するのではなく、右側(100)の評価結果と左側のパターン(A)を照合するのだ。
A = 100
上記の例だと、ぱっとイメージしにくいだろう。
パターン照合とイメージが似ているのは正規表現ではないかな。
正規表現を使うと、ある複雑な文字列からパターンにマッチする特定の部分を手軽に抽出することができる。
ただし、正規表現のルールが分かりにくい短所はある。
同じくErlangのパターン照合も正規表現のようなものだが、
文字列だけでなく、タプル、リスト、関数の引数など、何にでも使えると考えればいいだろう。
以下の例を見てみよう。
19> T = {{name, "duke"}, {age, 34}}. {{name,"duke"},{age,34}} 20> {{_,_},{_,MyAge}} = T. {{name,"duke"},{age,34}} 21> MyAge. 34
Tにタプルが二つ({name, "duke"}と{age, 34})が入っているが、
二つ目のタプルから年齢(34)を取得したい場合、20>のように
パターン照合すればいい。20>の左側にあるパターンのうち、
_(アンダーバー)はダミー要素で何でもマッチする。
マッチするだけで変数に束縛したくない場合、_(アンダーバー)を使う。
パターンのうち、大文字で始まっているMyAgeが
二つめのタプルにある2番めの要素(34)に一致して
年齢(34)を取り出せたことが分かる。
パターン照合の基本が分かれば、後はマニュアルか
ググってパターン照合の例を参考すればいいだろう。
最後に関数のパターン照合に関する例を載せる。
% func_test.erl -module(func_test). -export([add/2]). add(X,plus) -> X + X; add(X,pow) -> X * X; add(X,Y) -> X + Y + 1. % 以下はREPL環境で実行したもの。 1> c(func_test). {ok,func_test} 2> func_test:add(3, plus). 6 3> func_test:add(3, pow). 9 4> func_test:add(3, 5). 9 5> c(func_test). {ok,func_test}
ガード
ある関数を呼び出すと、呼び出される関数を探すため、
引数に指定した値の型と関数のシグネチャとのパターン照合を行う。
Javaのオーバーロードと似ているものだと考えればよさそう。
追加的にメソッド内で引数の値の内容によって、違う動作をする必要がある時、
Javaではif文やポリモーフィズムを使うが、Erlangではガードを使えばいい。
ここでは関数を中心にガードを使い方を説明するが、
case式やif式でも使用できる。
以下のソースを見てみよう。
% guard_module.erl -module(guard_module). -export([guard_test/1]). guard_test(X) when X < 5, X > 1 -> X * X; guard_test(X) when X >= 5; X < 10 -> X - 2; guard_test(X) -> X - 2.
上記のソースを解析してみると、
呼び出された関数がguard_testで一番目の引数(X)に値が指定されていて、
Xが5未満かつ1より大きければ(コンマ[,]はandを意味)、X * Xを、
Xが5以上か10未満なら(セミコロン[;]はorを意味)、X - 2を、
それ以外の場合(つまり、elseなら)、X - 2
を返すという意味だ。
以下は上記の関数をREPL環境でテストしたものだ。
1> c(guard_module). {ok,guard_module} 2> guard_module:guard_test(5). 3 3> guard_module:guard_test(1). -1 4> guard_module:guard_test(6). 4 5> guard_module:guard_test(15). 13 6>
ガードは条件文のようなものだと考えればいい。
ガードは便利そうだが、関数の宣言部が複雑になる恐れがあるので、
なるべくパターン照合だけで済ませた方がいいと思う。
今日はここまで。
Erlang 基礎ポイント1 - 紹介, 文法
1・2年ごとに新しい言語を勉強している。
Python・Scala・Lispに続けて、今年からはErlang.
メインはJava・Javascriptだが、他の言語を接することで
問題解決やアイデア作りに役立っているを実感している。
これから数回に分けて勉強した内容を書くつもり。
目標は網羅でなく、ポイントをつかむことだ。
Erlangとは
Erlangはエリクソン社が開発したプログラミング言語である。
少ないコード量で並列処理・ネットワーク処理を記述できる。
Linux・Windowsをはじめとして、色々なOSで動く。
参考書
練習問題が曖昧な部分があるが、大体読みやすい。
実行環境
- REPL(Read Eval Print Loop)ができるシェル(erl)を備えている。勉強や検証時にうってつけだ。
- プログラムがある程度規模がある場合、コンパイル(erlc)して実行(erl)する。
文法
行の最後にはピリオドを置く。
1> Fruit = "apple".
変数は大文字で始まる。
1> Fruit = "apple".
"apple"
小文字で始まると、アトムになる。
アトムはJavaのenum定数のようなものだと考えればいいだろう。
変数のようにメモリ番地を指すのではなく、
名前(リテラル)そのものが意味を持つ。
つまり、以下のappleはappleそのもので、メモリのある番地を
表すものではないのだ。
2> apple.
=を使った変数への代入は一回しかできない。
2回代入するとエラーになる。
10> Fruit = "apple".
"apple"
11> Fruit = "mango".
** exception error: no match of right hand side value "mango"
変数を不変にすることで、変数の値がどう変わるかをしなくていいので、
プログラムの把握がしやすい。
タプルは複数の要素を一つにまとめるもの。
Javascriptのオブジェクトリテラルに似ている。
2> Person = { name, "Duke" }.
{name,"Duke"}
実はErlangで=は代入でなくパターン照合を意味する。
6> Location = { "Japan", "Tokyo" }.
{"Japan","Tokyo"}
7> { Country, City } = Location.
{"Japan","Tokyo"}
8> Country.
"Japan"
9> City.
"Tokyo"
10> {A, atom} = { "Foo", atom}.
{"Foo",atom}
11> A.
"Foo"
Locationには、まだ値がバインドされていないので、Locationにタプルが入り、無事実行される
Locationに値が入っていたとしても、その値が{"Japan", "Tokyo"}なら、6行は真となる。
もし違う値が入っていたら、エラーとなる。
10行で左辺のatomは、ただのアトムであり、右辺のタプルと要素数を合わせるための
ダミーのようなものだ。
リストを作るためのネイティブ表記がある。
15> Planets = [{mercury, 1}, {venus, 2}, {earth, 3}].
[{mercury,1},{venus,2},{earth,3}]
16> [First,Second|Remaining] = Planets.
[{mercury,1},{venus,2},{earth,3}]
17> First.
{mercury,1}
18> Second.
{venus,2}
19> Remaining.
[{earth,3}]
22> lists:nth(1, Planets).
{mercury,1}
リストから要素を取り出したい時はパターン照合を使う。
listsモジュールのnthのような関数も使えるが、パターン照合の方が
Erlangらしくていいのでは?
今日はここまで。