最速で知る! ElixirプログラミングとErlang/OTPの始め方【第二言語としてのElixir】

Elixir入門の手引、第1弾となる今回はErlangのVM上のプロセスをElixirで扱う方法を説明し、Elixirでどのようにアプリケーションを構築するのかを解説します。

最速で知る! ElixirプログラミングとErlang/OTPの始め方【第二言語としてのElixir】

はじめまして! 大原常徳(おおはら・つねのり)といいます。 今回から2回に分けて「第二言語としてのElixir」というテーマで、プログラミング言語Elixirの入門記事をお届けします。

Elixirは、José Valim氏によって開発されているプログラミング言語です。 最大の特徴は、ErlangのVM上で動作し、Erlangのモジュールを利用できることでしょう。 ちょうど、ScalaがJava VM上で動作し、Javaの関数を利用できるという関係に似ていますね。

{$annotation_1}

{$annotation_2}Elixir

ErlangのVM上で動作することから、Elixirには次のような特徴が備わっています。

  • 耐障害性
  • 高可用性
  • 分散アプリケーションの構築のしやすさ

Erlangでは「プロセス間のメッセージパッシング」というErlang独自の概念をうまく使うことで、びっくりするくらいあっさりとこれらの特徴を実現しています。

そこでこの記事では、まずErlangのVM上のプロセスをElixirで扱う方法を説明してから、Elixirでどのようにアプリケーションを構築するのかを解説します。 はじめのうちは慣れない概念に戸惑うかもしれませんが、がんばって読み進めてみてください。

それでは、環境構築から始めましょう!

Elixirの動作環境を構築する

さまざまなプラットフォームでのインストール方法

プラットフォームごとに、Elixirのインストール手順を見ていきましょう。

ElixirはErlangのVM上で動作するので、 当然ですが実行にはErlangのVMが必要です。 一般的な環境であれば、Elixirのインストールと同時に、ErlangのVMもインストールされます。

macOSにElixirをインストールする

Macユーザーは、Homebrewを使って簡単にElixirをインストールできます。

# brewのパッケージを更新
$ brew update

# Elixirのインストール
$ brew install elixir

HomebrewでElixirをインストールすると、Erlangも関連パッケージとして同時にインストールされます。

WindowsにElixirをインストールする

Windowsユーザーは、Elixirの公式サイトのダウンロードページからWindows用のインストーラーをダウンロードし、実行してください。

サイトの説明にあるように「Click next, next, …, finish」と進めればインストールできます!

Unix/Linux環境にElixirをインストールする

Unix/Linuxユーザーは、各ディストリビューション毎のパッケージインストール方法で、Elixirパッケージをインストールしてください。

Dockerで環境を構築する

Dockerで動かす場合は、Elixirの公式イメージを利用するだけです。

このDocker環境を起動するには次のようにします。

$ docker run -it elixir

上記を実行すると、後述するIExというElixirの対話環境が、コンテナ内で起動した状態になります。

なお、次のように実行することで、IExではなくシェル(Bash)が起動された状態にすることも可能です。

# コンテナ内でbashを起動(このあとで`iex`と打てばIExが利用できる)
$ docker run -it elixir bash

対話環境のIExを起動してみよう

Elixirをインストールすると、対話環境(REPL)としてiexというコマンドが利用可能になります。 手元のインストール環境で、次のように起動してみましょう。

# iexを起動
$ iex

起動したら、プログラミング教育の伝統に則り、お約束の処理を実行してみましょう。

$ iex
Erlang/OTP 19 [erts-8.3.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.4.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> IO.puts "hello, world!"
hello, world!
:ok
iex(2)>

Erlang/OTP 19から始まる3行はiexの起動メッセージで、 iex(1)>iexのコマンドプロンプト、つまり入力待ち状態を表す表記です。 以後この起動メッセージは省略します。

上の例で入力されているIO.putsは引数を標準出力に出力する関数で、"hello, world!"は文字列です。

IExを終了するには、Ctl-Cを2回入力するか、Ctl-Gの後にqRetrunを入力します。

iex(2)>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution
^C
$

プロセスを使ってアプリケーションを構築する

アクターモデルという言葉を聞いたことがあるでしょうか?

アクターモデルとは並行計算モデルの1つで、すべての「もの」を「アクター」という構成要素で表現します。 ちょうど、オブジェクト指向プログラミングにおいて、あらゆる「もの」は「オブジェクト」であると考えることに似ています。

アクターがただの「もの」と違うのは、次のような特徴があることです。

  • 他のアクターにメッセージを送信メッセージパッシングできる(メッセージの送信先をメールアドレスと呼びます)
  • アクターは、受け取ったメッセージをキューに溜められる(このキューをメールボックスと呼びます)
  • アクターは、新たなアクターを生成できる
  • アクターは、並行に処理が実行される
{$annotation_3}

Elixirが並行処理や分散処理に強いといわれるのは、このアクターモデルを採用しているおかげです。 個々のアクターでは小さな仕事を担当し、それらアクター間での非同期なやり取りとして全体のプログラムを作れるので、出来上がったプログラムが自然と並行・分散システムになるというわけです。

そこで、ここからの説明では、まずElixirにおけるアクターモデルの使い方を説明していきます。 具体的には、アクター間でのメッセージのやり取りについて学びます。 しばらくの間は何の役に立つのか分からない話が続くと思いますが、ぜひ実際に手を動かしながら読み進めてください。

そうして「アクター間でのメッセージのやり取り」という世界観を掴んでしまえば、Elixirの力で簡単に並行・分散プログラミングを楽しめるようになります!

Elixirのアクターモデルとプロセス

Elixirのアクターモデルは、ベースとなるErlangのVM上で動作しています。Erlangではアクターのことを「プロセス」と呼ぶので、以降の説明でもプロセスという用語を使います(紛らわしいことに、ErlangのプロセスはOSのプロセスとはまったく別物なので注意してください)

Elixirのプロセス、つまりErlangのプロセスですが、メモリはデフォルトで309ワード、 起動にかかる時間は数マイクロ秒と、OSのプロセスに比べて非常に軽量です。

プロセスとプロセスの間では、何ができるでしょうか? プロセス間のやりとりとして実行可能な処理は、主に以下の5つです。

  1. プロセスから別のプロセスに対してメッセージを送信する
  2. プロセスからのメッセージを別のプロセスが受信する
  3. プロセスが別のプロセスを生成する
  4. プロセスが別のプロセスをリンクする
  5. プロセスが別のプロセスをモニタする

順番に見ていきましょう。

1. プロセスから別のプロセスに対してメッセージを送信する

Elixirのプロセスはそれぞれユニークな識別IDを持っており、このIDをプロセスID(PID)と言います。

プロセスIDを使って send <process-id>, <message>とすることで、 あるプロセスから別のプロセスに対してメッセージを送信できます。

また、自身のプロセスIDは、self()とすることで取得できます。

IExを立ち上げて試してみましょう。

# 自分自身のプロセスID
iex(1)> self()
#PID<0.83.0>

詳しくは後述しますが、iex自身も1つのプロセスなので、プロセスIDを持っています。 上記の例では、いま実行しているiexのプロセスIDが「0.83.0」であるということが読み取れます。

次に、この自分自身のプロセスに何かメッセージを送ってみましょう。 例として"my-message"という文字列を送ってみます。

#  自分自身のメールボックスに"my-message"を送信
iex(2)> send self(), "my-message"
"my-message"
iex(3)>

プロセスに送ったメッセージはどうなるのでしょうか? 次は受信側を見てみましょう。

2. プロセスからのメッセージを別のプロセスが受信する

送信したメッセージを受信してみます。

Elixirにはメッセージを受信する方法がいくつかありますが、 ここではメールボックス内のすべてのメッセージを表示、解放するflush()を使って確認してみましょう。

# 受信したメッセージ("my-message")を表示・解放
iex(4)> flush()
"my-message"
:ok

# 上でメッセージを解放したので何も起こらない
iex(5)> flush()
:ok

# メッセージを2つ送信
iex(6)> send self(), "my-message1"
"my-message1"
iex(7)> send self(), "my-message2"
"my-message2"

# 2つ分のメッセージが表示・解放
iex(8)> flush()
"my-message1"
"my-message2"
:ok
iex(9)>

3. プロセスが別のプロセスを生成する

プロセスは、spawn <モジュール>, <関数>, <引数の配列>として生成できます。 モジュールというのは、いくつかの関数をまとめて名前を付けたもののことです。 あるモジュールで定義された、ある関数に、引数を渡すことで、新しいプロセスを生成するわけです。

例として、モジュールSampleFuncと関数helloを定義してみましょう。 次のコードをsample_func.exというファイルに保存してください。

defmodule SampleFunc do
  def hello(person) do
    IO.puts "Hello, #{person}. My pid is #{inspect self()}."
    receive do
      message -> IO.puts "Message is #{message}."
    end
  end
end

Elixirでは、defmoduleでモジュールを、defで関数を定義します。

上記の例では、引数として渡された人物の名前personと、自身のプロセスIDを、挨拶の文字列に埋め込んで表示するだけの関数を定義しています。 このように、文字列中に#{xxx}を含めることで文字列を、#{inspect xxx}を含めることで文字列以外の値を埋め込めます。

receiveというのは、メッセージを待ち受けて、ブロック内の処理を行う仕組みです。 この例では、受け取ったメッセージの内容を標準出力へと書き出すのにreceiveを利用しています。

それでは、このSampleFuncモジュールを使って、プロセスの生成とメッセージの送信をしてみましょう。

iexは、引数にファイル名を指定すると、そのファイルをコンパイルしてロードした状態で起動します。 次のようにiex sample_func.exとすることで、sample_func.exで定義されているSampleFuncモジュールをロードした状態でiexが起動します。

$ iex sample_func.ex
# (1) プロセスを生成
iex(1)> pid = spawn(SampleFunc, :hello, ["田中太郎"])
Hello, 田中太郎. My pid is #PID<0.86.0>.
#PID<0.86.0>

# (2) メッセージを送信し、送信先のプロセスで処理を実行
iex(2)> send pid, "田中太郎さん、よろしくおねがいします"
"Message is 田中太郎さん、よろしくおねがいします."
"田中太郎さん、よろしくおねがいします"

# (3) メッセージを送信しても、送信先プロセスが終了しているので反応なし
iex(3)> send pid, "田中太郎さん、よろしくおねがいします"
"田中太郎さん、よろしくおねがいします"
iex(4)>

(1)では、spawnで新しく生成したプロセスのプロセスIDが返却されるので、それをpidとして保持しています。 この新たに生成されたプロセスでは、SampleFunc.hello関数が、"田中太郎"という引数で実行されます。 この関数は、名前とプロセスIDを表示した後、receiveでメッセージを待ち受けます。

(2)では、(1)で生成したプロセスに、sendを使ってメッセージを送信しています。 (1)で生成したプロセスは、メッセージを受け取ると、 receiveブロック内の処理(メッセージ内容の標準出力への書き出し)を行い、その後に終了します。

(3)では、プロセスに再度メッセージを送信しています。 しかし、(2)ですでにreceiveの待ち受けが終了しているので、何も起こりません。 このように、SampleFunc.helloの処理が完了すると、プロセスは終了します(つまり消えてしまいます)

このプロセスをずっと維持したい場合はどうすればよいでしょうか? 処理の最後で自分自身helloを呼び出せば、SampleFunc.helloがループされるので、プロセスが維持できそうです。

処理をループするバージョンの関数を、以下のようにhello2として定義してみましょう(なお、#から行末まではElixirのコメントです)

defmodule SampleFunc do
  def hello2(person) do
    IO.puts "Hello, #{person}. My pid is #{inspect self()}."
    receive do
      message ->
        IO.puts "Message is #{message}."
        hello2(person) # メッセージを受信し、処理が完了したら自分自身を呼び出す
    end
  end
end

前回と同じように実行してみます。

# プロセスを生成
iex(1)> pid = spawn(SampleFunc, :hello2, ["山田二郎"])
Hello, 山田二郎. My pid is #PID<0.86.0>.
#PID<0.86.0>

# メッセージを送信し、送信先プロセスで処理を実行
iex(2)> send pid, "二郎さん、ループしてますか?"
"Message is 二郎さん、ループしてますか?."
"二郎さん、ループしてますか?"
Hello, 山田二郎. My pid is #PID<0.86.0>.

# メッセージを送信し、送信先プロセスで処理を実行
iex(3)> send pid, "二郎さん、ループしてますか?"
"Message is 二郎さん、ループしてますか?."
"二郎さん、ループしてますか?"
Hello, 山田二郎. My pid is #PID<0.86.0>.

# メッセージを送信し、送信先プロセスで処理を実行
iex(4)> send pid, "二郎さん、ループしてますか?"
"Message is 二郎さん、ループしてますか?."
"二郎さん、ループしてますか?"
Hello, 山田二郎. My pid is #PID<0.86.0>.
iex(5)>

うまくいきました!

4. プロセスが別のプロセスをリンクする

プロセスの生成やメッセージの送受信というのは、なんとなくイメージが掴めそうですが、 プロセスを「リンク」することで一体何ができるのでしょうか?

プロセスを別のプロセスとリンクすると、プロセス同士が互いを監視するようになります。 これにより、一方のプロセスが死んだときに、もう一方のプロセスに対して終了メッセージ(終了シグナル)を送信できます。

{$annotation_5}

あるプロセスから、spawnではなくspawn_linkで別のプロセスを生成することにより、生成元のプロセスとリンクしたプロセスを生成できます。 iex上でspawn_linkを呼び出してプロセスを生成してみましょう。

iex(1)> pid = spawn_link(SampleFunc, :hello2, ["田中花子"])
Hello, 田中花子. My pid is #PID<0.86.0>.
#PID<0.86.0>

iex(2)> send pid, "こんにちは"
"Message is こんにちは."
Hello, 田中花子. My pid is #PID<0.86.0>.
"こんにちは"

このプロセスを終了してみましょう。 プロセス終了の命令Process.exit(<プロセスID>, <終了理由>)を実行することで、 引数のプロセスに終了シグナルが送信され、それを受け取ったプロセスが終了します。

iex(3)> Process.exit(pid, "終了しなさい")
** (EXIT from #PID<0.84.0>) "終了しなさい"

Interactive Elixir (1.4.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

iex上でspawn_linkにより生成したプロセスを終了すると、iexが再起動してしまいました、これはどういうことでしょう?

実は、iexもプロセスとして実装されているので、spawn_linkで生成されたプロセスはiexのプロセスとリンクされます。 この状態でプロセスが終了すると、リンクされているiexのプロセスに対して終了シグナルが送信されるのでiexが終了するのです。

{$annotation_5}

終了シグナルを受信しても終了しないプロセスのことをシステムプロセスと言います。

リンクしたプロセスが終了するたびにiexが終了(再起動)してはたまらないので、iexをシステムプロセスにしましょう。 Process.flag(:trap_exit, true)とすることで、自身(のプロセス)をシステムプロセスにできます。

iex(1)> pid = spawn_link(SampleFunc, :hello2, ["田中花子"])
Hello, 田中花子. My pid is #PID<0.86.0>.
#PID<0.86.0>

iex(2)> send pid, "こんにちは"
"Message is こんにちは."
Hello, 田中花子. My pid is #PID<0.86.0>.
"こんにちは"

iex(3)> Process.flag(:trap_exit, true) # システムプロセスにする、Process.flagの返り値は変更前のフラグ値
false
iex(4)> Process.exit(pid, "終了しなさい")
true

iex(5)> flush()
{:EXIT, #PID<0.86.0>, "終了しなさい"} # プロセスの終了メッセージ
:ok
iex(6)>

終了メッセージ{:EXIT, #PID<0.86.0>, "終了しなさい"}が受信されることを確認できました。うまくいったようです。

{$annotation_6}

5. プロセスが別のプロセスをモニタする

「リンク」がプロセス間の相互監視なのに対し、非対称の一方通行でプロセスを監視するのが「モニタ」です。 リンクとモニタには、相互に監視しあうのか、片方だけを監視するのかという違いがあります。

モニタされたプロセスを生成することで、そのプロセスが死んだときに生成元のプロセスに対してメッセージを送るようにできます。 逆に、生成元のプロセスが死んでも監視対象のプロセスには何も起こりません。

{$annotation_7}

リンクではなくモニタしたプロセスを生成するにはspawn_monitorを使います。

iex(1)> {pid, ref} = spawn_monitor(SampleFunc, :hello2, ["山田太郎"]) # (1)
Hello, 山田太郎. My pid is #PID<0.86.0>.
{#PID<0.86.0>, #Reference<0.0.4.318>}

iex(2)> send pid, "こんばんは"
"Message is こんばんは."
Hello, 山田太郎. My pid is #PID<0.86.0>.
"こんばんは"

iex(3)> Process.exit(pid, "終了してください") # (2)
true

iex(4)> flush() # (3)
{:DOWN, #Reference<0.0.4.318>, :process, #PID<0.86.0>, "終了してください"}
:ok
iex(5)>

(1)でspawn_monitorが返すのは、spawn_linkのときとは違って、単なるプロセスIDではなく{<プロセスID>, <リファレンス>}という組です。 この「リファレンス」というのは、グローバルに一意な参照値です。

(3)では、モニタされたプロセスに対し終了シグナルを送信して、プロセスを終了させています。 リンクのときと違って、生成元のプロセスであるiexは、終了メッセージを受け取っても終了しません。

(4)からわかるように、モニタしているプロセスが終了すると、 終了シグナルではなくダウンメッセージ({:DOWN, ...})が送信されるので、 監視元のプロセスは終了しないのです。

{$annotation_8}

GenServerから始めるOTP

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く