480 likes | 755 Views
プログラミング言語 Erlang と その応用事例. 檜山正幸 (HIYAMA Masayuki) 2008 年 10 月 23 日 ( 木 ). おおざっぱな予定. 前半: Erlang の紹介 18:30 - 前半の質疑応答 19:00 - 後半: Web Comunication Channels の紹介 19:15 - 後半の質疑応答 19:45 -. プログラミング言語 Erlang. Erlang は関数型言語 ? Erlang はプロセス指向言語 ! Erlang はメッセージ指向言語 ! Erlang は並列指向言語 !
E N D
プログラミング言語Erlangとその応用事例 檜山正幸 (HIYAMA Masayuki) 2008年10月23日(木)
おおざっぱな予定 • 前半:Erlangの紹介 18:30- • 前半の質疑応答 19:00- • 後半:Web Comunication Channelsの紹介19:15- • 後半の質疑応答 19:45-
プログラミング言語Erlang • Erlangは関数型言語? • Erlangはプロセス指向言語! • Erlangはメッセージ指向言語! • Erlangは並列指向言語! • Erlangは分散指向言語 • Erlangのランタイムシステムはすごい!? 文末記号の意味: ? 今日は強調しない。あまり触れない。 ! 強調したい。いろいろな言い方されるが根は一緒。 !? 確かにそうだが、よくわからない(からあまり触れない)。 無印 特に話題にしないが、推測できるでしょ(たぶん)。
Erlangプログラムの編成:なんだかんだ • ! モジュールは関数の集まり mod:func(arg1, arg2) • モジュール名、ソースファイル名、実行可能(BEAM)ファイル名は同一 • 論理的な概念(モジュール)と物理的な概念(ファイル)を混同しても弊害は少ない • ! ランタイムシステムには、モジュールが(原則)動的にロードされる • モジュールの集まりを”アプリケーション”と呼ぶ(要注意!)
Erlangプログラムの編成:なんだかんだ (2) • アプリケーションの物理的実体は、モジュールを格納しているディレクトリ • アプリケーションメタデータもあるが、小規模なアプリケーションではメタデータ不要 • システムはアプリケーション(モジュール集合)と、どのアプリケーションにも属さない野良モジュールからなる。 • パッケージ機構(モジュールに階層的名前空間を提供)もあるが、使っている例を見たことがない。 • ! 関数名は、同一モジュール内なら裸の名前、他のモジュールならコロンで修飾 • import機構があるが、「使わないほうがよい」とされている
Erlangプログラムの編成:だから結局 (図:ここでホワイトボードに図を描きます。)
Erlangのプロセス • どんな関数でも、とあるプロセスで実行される • 換言すると、プロセスがないと関数が実行できない • 一方、プロセスは1つの関数(メイン)を実行するためにある • もちろん、1つの関数から呼ばれた関数も同じプロセスで実行される • プロセスのメイン関数が終わればプロセスも終わる(消滅) Erlangでは、関数だけでなく、プロセスが明白に意識され市民権を持ち、 いくらでも(誇張あり)プロセスを使える。
ここでなぜかHelloWorld %% d.erl -*- coding:utf-8 -*- -module(hello). -compile(export_all). do() -> spawn(fun()->io:format("Hello, world.~n") end). do(N) -> spawn(?MODULE, hello, [N]). forever() -> spawn(fun helloever/0). hello(0) -> ok; hello(N) when N > 0 -> io:format("Hello, world.~n"), timer:sleep(500), hello(N - 1). helloever() -> io:format("Hello, world.~n"), timer:sleep(500), helloever().
ここでなぜかHelloWorld (2) • ErlangShell(という名の対話的プロセス)から hello:do(). とすると: • Shellプロセスの管理下で hello:do() が実行される。 • do()からプロセスが生成される。 • 生成されたプロセスにより、1回Helloを表示する。 • それだけでプロセスは正常終了。 • hello:do(10). とすると: • 基本的に同じことが起きるが、プロセスはやや長時間の寿命を持つ。
ここでなぜかHelloWorld (3) • 再帰呼び出しに制限を設けず、長時間を無限に伸ばすとリアクティブなプロセスになる。 • 寿命の観点から、プロセスを次の2種に分類するのが現実的: • ある一定時間(予測はできなくても)後に仕事を終えるプロセス。 • 原則として無限に動き続け、なんかのはずみで終わるかもしれないプロセス。
Erlangのプロセスって何? • 確かにOSプロセスの概念に近い • 「とっても小さなコンピュータ」という比喩が有効 • CPU(リダクション実行主体)とスタックとヒープ • 1個のプロセスは、1個のCPUと1つのスタックで逐次的に動く • 生まれて、動いて/働いて、消える • 数マイクロ秒で生成可能 • 普通に数万から数十万/ランタイム • 参考: http://lab.klab.org/wiki/Erlang_Process 「4000万個起動することが出来ました」
Erlangのプロセスって何? (2) • 実行コードは、関数/モジュールというまとまりに編成されている • スタック上で関数フレーム達が伸びたり縮んだり • ヒープにはデータの実体が確保されたり解放されたり • この小さなコンピュータ達はネットワーク環境にいる • 固有ID(アドレス)を持っている • 名前(公開ホスト名)を持ってもいい/持たなくてもいい • 互いに直接的にいきなりパケット(?)通信できる • パケットに例えたモノはErlangターム(任意) • バッファリングするNIC(?)を持っている、メッセージキュー/メールボックス
Erlangのプロセスって何? (3) • メモリ空間は互いに完全に分離されている • 原則的に(例外はあるが)共有メモリはない • 実行コード(関数)は共有、公共的で特に所有者プロセスはない • NICに相当するメッセージキューへのポストは直列化される • メッセージ以外の相互作用はなく、プロセス達は独立に動作する
Erlangのプロセスって何? (4) • 各プロセス(小さなコンピュータ)は、ほぼ同一の性能 • 実際にはランタイム内で、できるだけ公平にスケジュールされる • リソース割り当て(空間の分配)とスケジューリング(時間の分配)の対象/単位が同じ • 1個のプロセスを見てる限りは、並列性を意識する必要はない。 • メッセージのデータは、異なるメモリ空間にコピーされると解釈される。実際はそうでもないけど(理由:イミュータブル)。
メッセージ・データの移動 図-移動(ホワイトボード)
メッセージング・グラフ • 理論的には完全有効グラフだが、相手を知ってないとメッセージを送れないので、 「知っている」関係で通信の経路は制限される。 • メッセージデータは、特定時点のメッセージンググラフの辺に沿って伝送される。 全プロセスをノードするメッセージング・グラフは時々刻々と変化する。 • 図-メッセージング (ホワイトボード)
リンクとシグナルのグラフ • リンクされたプロセスは、片方が死ぬともう一方も死ぬ • exitシグナル、errorシグナルがリンクに沿って伝搬 • シグナルはメッセージと別物 • だが、メッセージとして捕捉可能 リンクの設定は、「linkするほう→されるほう」で方向を持つ。が、シグナルはその方向に無関係。
リンクとシグナルのサンプル %% d06.erl -*- coding:utf-8 -*- -module(d06). -compile(export_all). start() -> spawn(fun parent_main/0). parent_main() -> process_flag(trap_exit, true), Pid = spawn_link(fun child_loop/0), parent_loop(Pid).
リンクとシグナルのサンプル (2) parent_loop(Pid) -> receive s -> % stop exit(Pid, kill), % 通常、killは乱用してはいけない io:fwrite("Parent: byebye.~n"), exit(normal); % 明示的にexitを呼んでもよい {'EXIT', Pid, Why} -> % シグナルから変換されたメッセージ io:fwrite("Parent: Child ~p exited. :~p~n", [Pid, Why]), NewPid = spawn_link(fun child_loop/0), parent_loop(NewPid); Message -> Pid ! Message, % そのまま子供に転送 parent_loop(Pid) end.
リンクとシグナルのサンプル (3) child_loop() -> receive {error, Term} -> io:fwrite("Child ~p: 'error' received.~n", [self()]), erlang:error(Term), child_loop(); {exit, Term} -> io:fwrite("Child ~p: 'exit' received.~n", [self()]), exit(Term), child_loop(); {throw, Term} -> io:fwrite("Child ~p: 'throw' received.~n", [self()]), throw(Term), child_loop(); Other -> io:fwrite("Child ~p: Unknown message:~p~n", [self(), Other]), child_loop() end.
よく出来た資料があるので拝借 • 特に、アニメートするプロセスの図がわかりやすい。 • Erlang.ppt
Erlangでアプリケーションを作るには • 関数達の静的な構造はモジュール(+アプリケーション)として編成 • 実行時のプロセス達の生成消滅のシナリオを考える • メッセージグラフ、リンクグラフとしてプロセス達の相互関係を考える • メッセージのプロトコル、シグナルのプロトコルを考える
Erlangでアプリケーションを作るには (2) • 基本的なフレームワークはOTPライブラリが準備している。 • メッセージプロトコルは、RPCのAPIやイベント配信として考えるとよい • 高水準のAPIを(例えばIDLで)決めれば、あとはOTPが面倒みてくれる • 参照: http://d.hatena.ne.jp/m-hiyama/20070712/1184213007 • リンクグラフの構成とシグナルのプロトコルもOTPが準備している(ワーカー/スーパーバイザ・モデル)
Erlangでアプリケーションを作るには(3) • 図(ホワイトボード) • 基本素材:関数、プロセス、メッセージ • 高級でマクロな素材:RPC、イベント、クラアント/サーバ、スーパーバイザ・ツリー • ユーザーレベルでのAPI、プロトコル • アプリケーション
Web Communication Channelsの紹介 http://www.microapplications.net
ことの発端 • Webアプリケーションとはいうが • 作る(作らせる)のはWebサイト所有者 • ブラウザ利用者はサイトと独立にアプリケーションを作れないの?
ブラウザ上のアプリケーション • GUIはHTMLレンダリングエンジン • 実装言語はJavaScript • ストレージがない • 実は自発的に通信もできない • Ajaxは? • サイトに依存し縛られる
通信機能とストレージ • 中立な中継サイトを設けて、できるだけ透過的にブラウザ-ブラウザ通信をサポートする。 • 永続的なストレージも提供する。 • クロスドメインHTTP通信が必須 • 現状、変な方法しかない • 悲しいがしょうがない • 逆方向(サーバー ⇒ブラウザ)通信も必要 • COMET • 参考: http://d.hatena.ne.jp/m-hiyama/20080528/1211950144
別な動機 • サーバー側もJavaScriptで書けたらいいじゃない? • 既にあるけど流行ってない • JavaScriptプログラムがローミングしたら面白いのでは? • そりゃ無理でしょ • JavaScriptの小さな小さなサブセットくらいなら、、、、 • この未練は今でも尾を引いている
Erlangがお手本 • 単に実装言語としてだけでなく、分散モデルも借用。
アリモノをツギハギ • JavaScriptのイベントモデルはW3C DOM3から • 外部からのコールバックはActionScriptのExternalInterface • データ形式は徹底的にJSON、ただし抽象的データ形式として • OMG IDLのサブセットで仕様記述の予定(全然できてない) 100%コンフォーマントはめざしてないが、それでも標準と折り合いを付けるのは大変。 用語の混乱やネーミングの理不尽さに泣く。
COMET • 原理は簡単 • やってみると、ブラウザごとの挙動の違いに泣く • JavaScriptのシングルスレッドにも泣く -- IOブロックとかsleepしてポールとかができない • On Demand JavaScript方式(a.k.a JSONP)とCOMETを組み合わせて、なんとか双方向通信
COMETは資源を消費する • が、Erlangなら、、、 • http://www.sics.se/~joe/apachevsyaws.html • Apache vs. Yaws • Apache が同時接続数約4千で応答なし、Yaws 同時接続数8万以上まで応答。 • http://groups.google.com/group/comp.lang.functional/msg/33b7a62afb727a4f?dmode=source • 2000万個のプロセス(64-bit erlang on a 1.5 GHz SPARC with 16 GB RAM)
Erlangで作ったことは • たしかに考えやすい、作りやすい • 他の言語とものすごく変わるわけではない • スレッドのような難しさはない • 静的な型概念、型チェックがないのは痛いときもある • YAWSというプラットフォームもあることだし、Webアプリケーションには向いているのではないか • 逆に、Web的手法がErlangプログラミングに生かせることもある
Erlangで作ったことは (2) • 500メッセージ/秒くらいはさばけそう • フォールトトレランスや実行時コード置き換えは今後 • 最近やっと「不要なことは書かない」「いさぎよく死ぬ」「一人で死ぬ」が分かってきた • これは、Erlangのポリシーで納得が難しい部分
アプリケーション • ジャンケン大会が最初設定した目標だった • 多人数でやるクイズやゲーム • 同時アンケート/投票 • ブラウザ・サーバー(今日、「神社を作れ」と) • ブラウザ不在時はスクリプトで動く
問題 • クロスドメイン通信のような基盤があやしい • セキュリティ :よくわかんない • モラル : これも問題になるかもしれない • クラスター構成 : まだやってないので未知 • 監視 : ライブラリがあれども事例がない • なにがうれしい : さあ? • ←大問題では
細かいことは気にしない。 なんとなく雰囲気がわかればよい。 後でまた説明します。 まずはサンプルとデモ
関数を定義してみる %% d01.erl -*- coding:utf-8 -*- %% 再帰関数 -module(d01). % モジュールの宣言 -export([fact/1, append/2]). % エクスポートする関数の宣言 %% 階乗 fact(0) -> 1; % Prologerはセミコロンである点に注意 fact(N) when N > 0 -> % when ... はガード N * fact(N -1). %% リストの連接 append([], List) -> List; append([First|Rest], List) -> [First|append(Rest, List)].
データとしての関数(fun) %% d02.erl -*- coding:utf-8 -*- %% 高階関数、ラムダ式 -module(d02). -compile(export_all). % コンパイラに全部エクスポートするように指令 sumup(From, To, Fun) -> % 第3引数にfun lists:sum( lists:map(Fun, lists:seq(From, To))). make_inc(N) -> % 戻り値がfun fun (X) -> X + N end. % '->' を忘れるのだ(檜山だけ?) sq(N) -> % テスト用、平方する関数 N * N.
いたるところにパターンマッチ %% d03.erl -*- coding:utf-8 -*- %% パターンマッチ -module(d03). -compile(export_all). p1({Name, Age}) -> % 既にお馴染み、引数にパターン io:format("Your name: ~s~n", [Name]), % カンマは順次実行 io:format("Your age: ~p~n", [Age]). p2({Name, Age}) when is_list(Name), is_number(Age) -> % ガードで制約をきつく io:format("Your name: ~s~n", [Name]), io:format("Your age: ~p~n", [Age]); p2(Name) when is_list(Name) -> % 別なパターン&ガード io:format("Your name: ~p~n", [Name]). p3(X) -> % p3と同じ、case式はもっともよく使われる制御構造 case X of Name when is_list(Name) -> io:format("Your name: ~s~n", [Name]); {Name, Age} when is_list(Name), is_number(Age) -> io:format("Your name: ~s~n", [Name]), io:format("Your age: ~p~n", [Age]) end.
%% d05.erl -*- coding:utf-8 -*- %% 自発的に動き続けるプロセス -module(d05). -compile(export_all). start() -> spawn(fun main/0). main() -> receive b -> % break break(); % 再帰じゃないけど末尾呼び出し (last call) s -> % stop io:format("stop.~n"); % プロセスも自然終了 _Other -> main() % キューのフラッシュ after 500 -> % 0.5秒ごとに io:format("Hello.~n"), main() % 末尾再帰 end. break() -> receive c -> % continue main(); % これでmainに戻る _Other -> io:format("break and stop.~n") % 終わり end. プロセス作ってメッセージ送ってみる
リアクティブなプロセス %% d05.erl -*- coding:utf-8 -*- %% 自発的に動き続けるプロセス -module(d05). -compile(export_all). start() -> spawn(fun main/0). main() -> receive b -> % break break(); % 再帰じゃないけど末尾呼び出し (last call) s -> % stop io:format("stop.~n"); % プロセスも自然終了 _Other -> main() % キューのフラッシュ after 500 -> % 0.5秒ごとに io:format("Hello.~n"), main() % 末尾再帰 end. break() -> receive c -> % continue main(); % これでmainに戻る _Other -> io:format("break and stop.~n") % 終わり end.
なぜかJavaクラスを出してみる /* Counter.java */ public class Counter { private int count; // 内部状態 public Counter(int init) { count = init; } public void up() { count++; // 状態の変更 } public void down() { count--; // 状態の変更 } public int value() { return count; // 問い合わせに応える } }
Erlangならこうなる %% counter.erl -*- coding:utf-8 -*- %% カウンタ -module(counter). -compile(export_all). % お行儀悪い %% new Constructor(init) 相当 start(Init) -> spawn(?MODULE, main, [Init]). start(Name, Init) -> register(Name, spawn(?MODULE, main, [Init])). main(Count) -> receive up -> % up() 相当 main(Count + 1); down -> % down() 相当 main(Count - 1); {value, Pid} -> % value() 相当 Pid ! {count, Count}, % 値をメッセージで返す main(Count); s -> % 終了 ok; _Other -> % 無視 main(Count) end.