cv.cm

コーディング · プログラミング言語

Erlang入門:初心者向けチュートリアル

約11分で読めます

結論

Erlangは、電話交換機やメッセージングサーバーのように、止まってはならないシステムのためにEricssonで作られた、関数型で並行処理向きの言語です。プログラムはBEAM仮想マシン上で多数の軽量プロセスとして動き、プロセス同士はメッセージで通信し、失敗するとスーパーバイザーが再起動します。このページでは、モジュールのコンパイル、型、パターンマッチ、再帰、リストとマップ、プロセス、OTP、エラー処理を扱います。

Erlangとは何か、何に使うのか

Erlangは、止まらずに動き続けなければならない電話交換機のために、1980年代にEricssonで設計された、関数型の動的型付け言語です。1998年にオープンソースとして公開されました。Erlangのプログラムは、何も共有せずメッセージで通信する多数の軽量プロセスとして、BEAM仮想マシン上で動きます。また、耐障害性のあるシステムを作るためのライブラリと設計原則であるOTP(Open Telecom Platform)が付属しています。

Erlangは、通信機器、メッセージングプラットフォーム(WhatsAppのバックエンドがErlangで作られたことは有名です)、RabbitMQのようなメッセージブローカー、RiakやCouchDBのようなデータベース、決済やIoTのシステムで使われています。ElixirとGleamもBEAM上で動き、Erlangのライブラリを直接使えます。

Erlangのインストールとhello, worldの実行

Erlang/OTPは、erlang.orgからか、パッケージマネージャー(たとえばbrew install erlangやapt install erlang)でインストールします。コードはモジュールに書きます。name.erlというファイルがnameモジュールを定義し、公開(エクスポート)する関数を列挙します。

% hello.erl
-module(hello).
-export([main/0]).

main() ->
    io:format("Hello, world!~n").
erlc hello.erl                          # compiles to hello.beam
erl -noshell -s hello main -s init stop
# Hello, world!
erl                                     # interactive shell; c(hello). then hello:main().

コメントは%で始まります。書式文字列の~nは改行、~pは任意の項を出力します。シェルでは、すべての式の末尾にピリオドを付けます。

記号・変数・基本の型

Erlangの記号は、初心者が最も驚くところです。カンマは式を区切り、セミコロンは節を区切り、ピリオドは関数やシェルのコマンドを終わらせます。変数は大文字またはアンダースコアで始まり、一度だけ束縛できます。

1> Name = "Erlang".
"Erlang"
2> Year = 1986, Next = Year + 1.
1987
3> Year = 2000.
** exception error: no match of right hand side value 2000
4> 10 / 4.
2.5
5> {10 div 4, 10 rem 4}.
{2,2}
6> is_atom(ok).
true
7> <<"binary string">>.
<<"binary string">>
型例補足
integer42, 16#FF任意精度。16進数は16#で書きます
float3.1464ビット倍精度
atomok, error, 'Hello'名前付き定数。小文字で始めるか、引用符で囲みます
tuple{ok, 42}固定サイズの値のまとまり
list[1, 2, 3]連結リスト。"abc"のような文字列は整数のリストです
map#{name => "Ann"}キーと値のストア
binary<<"text">>バイト列。テキストを効率よく保持する方法です

trueとfalseは別の型ではなく、アトムです。=は代入ではなくマッチなので、3行目は失敗します。Yearはすでに1986に束縛されているからです。

パターンマッチ・case・ガード

パターンマッチは、タプル、リスト、マップを分解し、どの節を実行するかを選びます。whenで書くガードは、節に条件を加えます。caseとifは値を返す式です。

-module(match).
-export([describe/1, grade/1, demo/0]).

describe({ok, Value}) -> io_lib:format("ok: ~p", [Value]);
describe({error, Reason}) -> io_lib:format("error: ~p", [Reason]);
describe(_Other) -> "unknown".

grade(Score) when Score >= 90 -> a;
grade(Score) when Score >= 70 -> b;
grade(_) -> c.

demo() ->
    [First | Rest] = [1, 2, 3],
    #{name := Name} = #{name => "Ann", age => 31},
    Size = case length(Rest) of
               0 -> empty;
               N when N < 5 -> small;
               _ -> large
           end,
    io:format("~p ~p ~s ~p ~p~n", [First, Rest, Name, Size, grade(75)]),
    io:format("~s~n", [describe({error, timeout})]).
% match:demo() prints:
% 1 [2,3] Ann small b
% error: timeout

最後の節を除き、すべての節は;で終わり、最後の節は.で終わります。どの節にも合わない場合、プロセスはfunction_clauseまたはcase_clauseエラーを発生させます。

関数・再帰・高階関数

関数は名前とアリティで識別され、name/Arityと書きます。ループはなく、繰り返しは再帰で行い、末尾再帰の関数は一定のスタック領域で動きます。無名関数はfun ... endと書きます。

-module(fns).
-export([factorial/1, sum/1, demo/0]).

factorial(0) -> 1;
factorial(N) when N > 0 -> N * factorial(N - 1).

sum(List) -> sum(List, 0).          % public wrapper
sum([], Acc) -> Acc;                 % tail-recursive helper sum/2
sum([H | T], Acc) -> sum(T, Acc + H).

demo() ->
    Double = fun(X) -> X * 2 end,
    io:format("~p~n", [factorial(20)]),                    % 2432902008176640000
    io:format("~p~n", [sum([1, 2, 3, 4])]),                % 10
    io:format("~p~n", [Double(21)]),                       % 42
    io:format("~p~n", [lists:map(Double, [1, 2, 3])]),     % [2,4,6]
    io:format("~p~n", [[X * X || X <- [1, 2, 3, 4, 5], X rem 2 =:= 1]]). % [1,9,25]

最後の行ではリスト内包表記を使っています。比較演算子に注意してください。=:=と=/=は厳密な等価・不等価を調べ、==と/=は数値を値で比較します(そのため1 == 1.0は真です)。「以下」は<=ではなく=<と書きます。

リスト・マップ・標準ライブラリ

listsモジュールにはmap、filter、foldl、sort、reverseなどがあります。mapsはマップを、stringとbinaryはテキストを扱います。完全なリファレンスはOTPのドキュメントにあります。

1> L = [5, 3, 8, 1].
[5,3,8,1]
2> lists:sort(L).
[1,3,5,8]
3> lists:filter(fun(X) -> X > 2 end, L).
[5,3,8]
4> lists:foldl(fun(X, Acc) -> X + Acc end, 0, L).
17
5> M = #{ann => 31, bo => 25}, map_size(M).
2
6> M2 = M#{cy => 40}, map_size(M2).
3
7> maps:get(bo, M2).
25
8> maps:get(zed, M2, none).
none

M#{cy => 40}は新しいマップを作り、M自体は変わりません。すでに存在するキーだけを更新したいときは、=>の代わりに:=を使います。

プロセスとメッセージパッシング

プロセスはErlangを代表する機能です。spawnが新しいプロセスを起動し、!がメッセージを送り、receiveがパターンでメッセージを選んで待ちます。プロセスは軽量なので、システムは非常に多くのプロセスを動かせます。リンクとモニターにより、あるプロセスは別のプロセスが死んだことに気づけます。OTPのスーパーバイザーはこれを使って、失敗したワーカーを自動的に再起動します。

-module(counter).
-export([start/0, loop/1]).

start() ->
    Pid = spawn(counter, loop, [0]),
    Pid ! {add, 5},
    Pid ! {add, 10},
    Pid ! {get, self()},
    receive
        {count, N} -> io:format("count = ~p~n", [N])   % count = 15
    after 1000 ->
        io:format("timeout~n")
    end.

loop(Count) ->
    receive
        {add, N} -> loop(Count + N);
        {get, From} -> From ! {count, Count}, loop(Count)
    end.

この手書きのループが、まさにOTPのgen_serverビヘイビアが一般化しているものです。本番のコードでは、gen_serverモジュールを書いてsupervisorの配下に置きます。

エラー処理と「let it crash」

Erlangの例外には、error、throw、exitの3種類があります。想定内の失敗は、通常{ok, Value}や{error, Reason}のタプルで返します。想定外の失敗は、捕捉しないことも多くあります。プロセスがクラッシュし、スーパーバイザーが正常な状態で再起動するからです。どうしても捕捉が必要なときはtry ... catchを使います。

1> file:read_file("missing.txt").
{error,enoent}
2> try 1 / 0 catch error:badarith -> division_failed end.
division_failed
3> try throw(my_reason) catch throw:R -> {caught, R} end.
{caught,my_reason}
4> case file:read_file("missing.txt") of {ok, Bin} -> Bin; {error, Why} -> Why end.
enoent

小さな完成プログラム:単語の出現頻度

このモジュールは、文字列中の単語をマップで数え、回数順に並べて、よく出る上位3語を表示します。文字列関数、lists:foldl、マップ、独自の並べ替えを組み合わせています。

-module(wordfreq).
-export([top/2, main/0]).

top(Text, N) ->
    Words = string:lexemes(string:lowercase(Text), " .,;:!?\n"),
    Counts = lists:foldl(fun(W, Acc) ->
                                 maps:update_with(W, fun(C) -> C + 1 end, 1, Acc)
                         end, #{}, Words),
    Sorted = lists:sort(fun({W1, C1}, {W2, C2}) ->
                                {-C1, W1} =< {-C2, W2}
                        end, maps:to_list(Counts)),
    lists:sublist(Sorted, N).

main() ->
    Text = "The cat and the dog. The dog sat, and the cat ran.",
    [io:format("~s: ~p~n", [W, C]) || {W, C} <- top(Text, 3)],
    ok.
% the: 4
% and: 2
% cat: 2

次に学ぶこと

  • erlang.orgにあるErlang/OTPのドキュメント。Getting StartedガイドとOTP Design Principlesが含まれます。
  • 『Learn You Some Erlang for Great Good!』。無料のオンライン書籍です。
  • 標準のビルドツールrebar3と、Elixirと共通のパッケージマネージャーHex。
  • 実際のサービスを作る段階での、gen_serverとsupervisorのリファレンスページ。

よくある質問

Erlangは今でも使われていますか?

はい。Erlangは今も通信システム、メッセージングサービス、RabbitMQを支えています。また、Erlangのライブラリを使えるElixirやGleamを通じて、BEAMのエコシステムは成長を続けています。

ErlangのOTPとは何ですか?

OTPは、Erlangに付属するライブラリ、ツール、設計原則の集まりです。gen_server、supervisor、applicationといったビヘイビアを提供し、耐障害性のあるプログラムの構造を決めます。

ErlangとElixirのどちらを学ぶべきですか?

Elixirは構文が親しみやすく、Web関連のエコシステムが大きいです。Erlangは小さく、OTPと多くの中核ライブラリが書かれている言語です。どちらも同じ仮想マシンで動き、片方を知っていればもう片方も読みやすくなります。

let it crashとはどういう意味ですか?

プロセスの中で起こりうるすべてのエラーを処理しようとせず、プロセスを失敗させて、スーパーバイザーにクリーンな状態で再起動してもらうという考え方です。エラー処理のコードは小さく保たれ、システムは自動的に回復します。

Erlangの変数が大文字で始まるのはなぜですか?

Erlangは、大文字・小文字で変数とアトムを区別します。Nameは変数で、nameは定数値のアトムです。

コーディング講座の一覧