メインコンテンツへスキップ

Erlang 123

小山 (ArcBlock 后端工程师)
Erlang

著者 小山(ArcBlock バックエンドエンジニア)

以前、「程序人生」の公式WeChatアカウントで短い記事Erlang入門ガイドを執筆し、6つのErlang関数を通してErlangの世界観を紹介しました。ArcBlockでは、バックエンドサービスをElixirで構築しています。Elixirを使う以上、当然Erlangにも関わります。このブログ記事は、私たちのTechnical Learning Series技術研修をもとに、3つのErlang関数を使ってErlangの世界観をご紹介します。

私たちの世界

私たちが暮らす世界は並行する世界です。人類全体で一つの社会を構成すると同時に、私たち一人ひとりも独立した個人です。個人にはそれぞれ違いがあり、私たちは皆、独立した思考を持ち、私とあなたの間で記憶が共有されることはありません。たとえば、新郎と新婦は結婚という出来事を一緒に経験しますが、新郎の記憶には新郎の視点で経験した結婚式が保存され、新婦の記憶には新婦の視点で経験した結婚式が保存されます。新郎と新婦はそれぞれ結婚式の記憶のコピーを持っていますが、互いに記憶を共有してはいません。

同時に、人と人とのコミュニケーションは完全に非同期で行われます。私があなたにWeChatメッセージを送った後も、私はそのままモーメンツを見続けますし、あなたもメッセージを受け取ってすぐに返信するとは限りません。情報の伝達は完全に非同期です。電話のようないわゆる同期的な情報伝達も、あなたが一言話し、次に私が一言話すというやり方で会話することを、人為的な慣習として決めているだけです。私はあなたが話すのを待たずに一方的に話し続けることもできます。その場合、あなたは私を失礼だと思うでしょうが、情報伝達の本質はやはり非同期伝達なのです。

プログラミング言語

コンピューターが登場してから、私たちはプログラミング言語を手に入れました。その目的は、プログラミング言語を通してコンピューターに何をすべきか伝えられるようにすることです。当初はアセンブリ言語を使ってコンピューターに何をすべきか直接指示していましたが、それは非常に面倒でした。そこで高級プログラミング言語を発明し、人間が理解できる方法でコードを書き、コンパイラーがそれを機械語に翻訳して対応する命令を実行するようにしました。学校で触れる高級言語の多くはオブジェクト指向です。オブジェクト指向言語は、オブジェクトを使って現実世界をモデル化しようとし、オブジェクトはクラスから生まれます。たとえば、机の上のコップはコップクラスのインスタンスであり、そのクラスにはコップの容量などの属性があり、水を入れる、水を注ぐといったメソッドがあります。これは現実世界をモデル化する非常に良い方法ですが、オブジェクト指向言語の最大の問題は、相手(オブジェクト)が見つからないことです。

一方、Erlangは別の角度、つまりプロセスを使って現実世界をモデル化しようとします。各プロセスは独立した個体であり、独自の状態を持ち、互いに一切の情報を共有しません。プロセス間の唯一の通信手段は、非同期メッセージパッシングです。Erlangの世界にはクラスもオブジェクトもなく、あるのは一つひとつのプロセスだけです。Erlangによるこの世界のモデル化方法は、私たちが普段暮らしている現実世界と実によく似ています。そのため、Erlangの世界観を理解することは、むしろ自然で直感的です。それでは、Erlangの世界観をコードで表すとどのようになるのか見てみましょう。

hello world

まず、最も簡単なhello worldがどのようなものか見てみます。

-module(foo).
-export([hello/0]).

hello() ->
    io:format("world~n").

ターミナルで実行してみます。

$ erl
1> c(foo),foo:hello().    % c(foo)はfooモジュールをコンパイルしてターゲットファイル.beamを生成するために使います
                          % 変更するたびに再コンパイルして最新のターゲットファイルを読み込む必要があります
world
ok

次に、io:format/1を呼び出す前に待機関数を追加し、1秒待つようにします。

hello() ->
    timer:sleep(1000),    % 1000ミリ秒は1秒です
    io:format("world~n").

もう一度ターミナルから呼び出します。

2> c(foo),foo:hello().
world                     % 1秒後に出力されます
ok

3回呼び出すと、3秒かかります。

3> foo:hello(),foo:hello(),foo:hello().
world                     % 1秒後に出力されます
world                     % 2秒後に出力されます
world                     % 3秒後に出力されます
ok

spawn

それでは、今日取り上げる最初の関数spawn/3を見てみましょう。この関数はプロセスを起動し、そのプロセスが一つの関数を実行します。

4> spawn(foo,hello,[]).   % 渡す3つの引数は、それぞれモジュール名、関数名、関数の引数です
                          % hello関数には引数がないため、3番目の引数には空を表す[]を渡します
<0.105.0>                 % 返されたこれはpidと呼ばれ、プロセスのid(process id)を表します
world                     % 1秒後に出力されます

3回呼び出すと、どうなるか見てみましょう。

5> spawn(foo,hello,[]),spawn(foo,hello,[]),spawn(foo,hello,[]).
<0.111.0>                 % pidが返されます
world
world
world                     % 1秒後に3つのメッセージが同時に出力されます

つまり、spawn/3を3回呼び出すことで3つのプロセスを起動し、それぞれのプロセスが1秒待ってからメッセージを出力するため、3つのメッセージが同時に出力されます。これが並行処理です。

したがって、Erlangにおける並行処理の概念は非常にシンプルで、spawn関数を呼び出すだけです。100個のプロセスに同時に処理をさせたいなら、spawnを100回呼び出すだけです。単純明快です。

send

プロセスが起動するとpidが割り当てられ、そのpidを通してプロセスにメッセージを送ることができます。これは、電話番号を使って誰かに電話をかけられるのと同じです。実際、Erlang shellを起動すると、shell自体も一つのプロセスであるため、自分自身にメッセージを送ることができます。

6> self() ! 1.                        % self()は自分自身のpidを返し、感嘆符はメッセージ送信を表します
1
7> process_info(self(),messages).     % 自分のメールボックス内のメッセージを確認できます
{messages,[1]}                        % 先ほど送信した1がメールボックスに入っていることがわかります

以上が非同期送信であり、Erlangにおけるプロセス間通信の唯一の手段です。

receive

メールボックス内のメッセージを受け取るにはどうすればよいでしょうか?receive関数を使います。

8> receive Msg -> Msg end.
1

以上が、今日取り上げる3つの関数です。とてもシンプルですが、これらによってErlangの世界観が構築されています。この世界はプロセスで構成され、プロセス間では一切の情報を共有せず、唯一の通信手段として非同期メッセージパッシングを利用します。

Erlangで何ができるのか?

この3つの関数がわかったところで、次は何でしょうか?Erlangは何に使えるのでしょうか?先ほどのhello/0関数を次のように少し変更します。

hello() ->
    receive
        {Msg,From} ->                            % {Msg,From}という形のあらゆるメッセージに対して
            io:format("~p: ~p~n",[From,Msg]),    % それを出力します
            hello()
    end.

新しいターミナルを開き、今度はいくつかの引数を付けます。

$ erl -sname a                                   % -snameで名前を付けられます
(a@sbs)1> c(foo),P = spawn(foo,hello,[]).        % sbsは私のマシン名、aはこのErlangノードの名前です
<0.107.0>                                        % これらが合わさって、私のErlangノード名a@sbsになります
(a@sbs)2> register(a,P).                         % このプロセスにaという名前を登録します
true
(a@sbs)3> a ! {1,node()}.                        % aという名前にメッセージを送れます。node()はノード名を返します
a@sbs: 1                                         % プロセスがこのメッセージを受信すると出力します
{1,a@sbs}

もう一つターミナルを開き、bという名前を付けます。

$ erl -sname b
(b@sbs)1> a ! {1,node()}                         % aという名前にメッセージを送ります
** exception error: bad argument                 % エラーになります。b@sbsノードにはaというプロセスがないためです
     in operator  !/2
        called as a ! {1,b@sbs}
(b@sbs)4> {a,a@sbs} ! {1,node()}.                % aというプロセスはa@sbs内にあるので、a@sbsの
{1,b@sbs}                                        % ノード内にメッセージを送ってみます

このとき、bノードではメッセージが出力されていませんが、aノードのターミナルウィンドウをもう一度見てみます。

b@sbs: 1                                         % メッセージがaのターミナルウィンドウに出力されました
(a@sbs)4>

わずか数行のコードで、最も簡単なチャットプログラムを実現しました。原理は実にシンプルで、あるノードのあるプロセスにメッセージを送り、そのプロセスがメッセージを受け取ると出力するというものです。Erlangの簡潔さは、言語のレベルでは開発者が基盤となる通信プロトコルを意識する必要がなく、どのメッセージをどこへ送るか、そしてメッセージを受信した後にどう処理するかだけに集中すればよい点にあります。

この記事に対応する研修動画はこちらでご覧いただけます。Erlang/Elixirを使ってブロックチェーンプロジェクトを構築することに興味がある方は、ぜひArcBlockにご参加ください