Erlang入門
Erlangの特徴は並列処理を言語やVMでサポートしている関数型言語です。
また、ErlangはPrologやその後継である並列論理型言語の強い影響を受けていて構文やVMの構造などにその影響が見られます。
インストール
Windows
http://www.erlang.org/downloads
よりインストーラをダウンロードしてインストールしてください。
Mac
brew install erlang
でインストールできます。
Linux
Debian系なら
apt install erlang
対話環境
Windows
"スタート" -> "全てのプログラム" -> "Erlang OTP " -> "Erlang"
Mac & Linux
ターミナルより
$ erl
と打ち込むことで対話環境が立ち上がります。
ためしに 1+2+3.
と打ち込んで見ましょう。
$ erl
Erlang R16B03 (erts-5.10.4) [source] [64-bit] [async-threads:10] [kernel-poll:false]
Eshell V5.10.4 (abort with ^G)
1>
6
2>
6
と表示されます。これは式1 + 2 + 3.
が計算された結果です。Erlangでは;
の代わりに.
が使われます。
コンパイル
erlc
コマンドでコンパイルします。
ErlangはBEAMというVM上で実行されるので、VM上で動作するバイナリにコンパイルされます。
こうして出来上がったファイルをbeamファイルと呼びます。
作成したファイルは
$ erl -pz . -noshell -noinput -s モジュール名 関数名 -s init stop
とタイプしてターミナルで実行することができます。
しかし、手軽に実行させたい場合はescript
を使用するほうが便利でしょう。
escript
escriptではErlangのプログラムをPerlスクリプトのように実行することができます。
#!/usr/bin/env escript
%% -*- erlang -*-
%%! -smp enable -sname factorial -mnesia debug verbose
main([String]) ->
try
N = list_to_integer(String),
F = fac(N),
io:format("factorial ~w = ~w\n", [N,F])
catch
_:_ ->
usage()
end;
main(_) ->
usage().
usage() ->
io:format("usage: factorial integer\n"),
halt(1).
fac(0) -> 1;
fac(N) -> N * fac(N-1).
$ chmod u+x factorial
$ ./factorial 5
factorial 5 = 120
$ ./factorial
usage: factorial integer
$ ./factorial five
usage: factorial integer
$ escript factorial 5
などとします。
詳しくはescript manual1を参照してください。
整数と実数
4> 2#0101.
5
5> 16#deadbeaf.
3735928495
のように#
のまえに数字を置くことで36進数までN進数表記ができます。
Erlangは多倍長整数です。
浮動小数点数はIEEE754形式で$1.0e-323$から$1.0e+308$の範囲です。
文字列
文字はASCII文字の前に$
を付けます。
7> $a.
97
文字列はダブルクォートで囲みます。これは文字のリストの糖衣構文です。リストは何かは後述します。
8> "a".
"a"
9> [$a,$b].
"ab"
Erlangの文字列は整数型のリストの糖衣構文であるため、多バイト文字は表現できません。
後述するバイナリで表現する必要があります。
変数
Erlangの変数名は大文字から始まります。
10> X = 1.
1
11> X.
1
これは変数X
に1
を代入して、X
を評価しています。
代入については後述するパターンマッチで説明します。
演算子
算術演算子
-
+
加算 -
*
乗算 -
-
減算 -
/
除算 -
div
整数の商 -
rem
整数の商の余り
基本的にC言語やJavaと同じです。div
とrem
はそれぞれ整数で割ったときの商と余りです。
比較演算子
-
>
大なり -
<
小なり -
>=
大なりイコール -
=<
小なりイコール -
==
等しい -
/=
等しくない -
=:=
等しい(型チェック付き) -
=/=
等しくない(型チェック付き)
論理演算子
-
not
否定 -
and
論理積∧ -
andalso
論理積∧(短絡) -
or
論理和∨ -
orelse
論理和∨(短絡) -
xor
排他的論理和
条件分岐
if
if
<条件式> -> <式1>;
<条件式> -> <式2>
end
Erlangにはパターンマッチがあるためif
はあまり使われません。
条件分岐は後述するパターンマッチの方をよく使います。
アトム
アトムは識別子を表します。小文字英文字から始まる英数字か、記号を含む場合はバッククォートで囲む必要があります。
1> abc.
abc
もちろん変数に代入もできます。
2> X = abc.
abc
3> X.
abc
アトムは後述するタプルやメッセージなどをパターンマッチするときに便利なのでErlangプログラミングではよく使用されます。
出力
Erlangではコンソールへの出力は一般にはio:format
という関数を使って行います。
io:format
Erlangによる書式つき出力はio:format
をつかって行われます。C言語のprintf
に相当しますが、フォーマット付き出力の書式はCよりCommon Lisp//コモン リスプに似ています。
io:format("~p~n",[Term]).
-
~p
プリティープリント -
~s
文字列 -
~n
改行 -
~d
整数(10進数)
ErlangにはC言語のような可変引数がないのでリストを使います。
パターンマッチ
case節
case <式> of
<パターン1> -> <式1>;
<パターン2> -> <式2>
end
case節はパターンマッチの一番わかり易い形式です。
2> A = 3.
3
3> case A of 1 -> a; 2 -> b; 3 -> c end.
c
とするとc
が返ります。
このようにC言語のswitch
文のように使うことも出来るのです。
この式から最後のパターンマッチの処理を除くとどうなるでしょうか?
4> case A of 1 -> a; 2 -> b end.
** exception error: no case clause matching 3
5>
このように例外が発生します。これはパターンマッチにマッチする条件がなかったと言う意味の例外です。
ワイルドカード
では次のように変更するとどうでしょうか?
5> case A of 1 -> a; 2 -> b; _ -> c end.
c
_
はワイルドカードといってあらゆる場合にマッチします。正規表現の.*
に似ています。予期しない値などのエラー処理に使われることがよくあります。
単一代入
X = 1.
先程代入といったのは実はパターンマッチなのです。
=
演算子は左辺の式に対して右辺のパターンマッチを行う演算です。左辺の変数がまだ束縛されてなければ右辺の式を評価した値が束縛されます。
=
の左辺式のパターンマッチが失敗するともちろん例外が発生します。
この例では数値型のような単純な型の場合ばかりなので有用性が分かりづらいと思います。
後述するリストやレコード、タプルなどと組み合わせることで非常に多様な使い方ができます。
パターンマッチはErlangの中核となる機能です。
関数
<関数名>(仮引数...) ->
<本体>.
例えば以下は引数に指定された変数をインクリメントする関数です。
inc(X) -> X+1.
このようにして関数定義を行います。
関数とパターンマッチング
Erlangは関数の引数にもパターンマッチを行うことができます。
signal(red) -> stop;
signal(blue) -> do;
signal(yello) -> carefull;
signal(_) -> error.
再帰
ErlangにはC言語におけるwhile
文などのループ制御構文がないので、ループ処理は再帰で行います。
階乗を求める関数を書いてみます。
pow(1) -> 1;
pow(X) -> X*pow(X-1).
末尾再帰
末尾再帰という形式にすることによってループ処理を最適化できます。
pow_tail(1,Ret) -> Ret;
pow_tail(X,Ret) -> pow_tail(X-1,X*Ret).
末尾再帰形式とは簡単にいうと、再帰関数の帰り値を評価しないのでスタックフレームに関数の戻り先を毎回積まなくてもいいので、
内部でwhile
ループのように扱えるのです。気になった人は自分で勉強してください。
レコード
いわゆる構造体です。
-record(<レコード名>, {<要素名1>,<要素名2>,...}).
レコードを定義するrd
関数と言う糖衣構文も用意されています。
rd(item, {id,name,price}).
7> rd(player, {name,hp}).
player
初期化
#<レコード名>{<要素名1>=初期値1, <要素名2>=初期値2,...}
といった形でレコードの初期化が可能です。
9> P = #player{name="hoge", hp=3}.
#player{name = "hoge",hp = 3}
参照
変数#<レコード名>.<要素>
と言った形で参照します。
10> P#player.name.
"hoge"
変更
変数#<レコード名>{<要素>="hage"}.
とすることでレコードの要素を変更したレコードを返します。
11> P#player{name="hage"}.
#player{name = "hage",hp = 3}
レコードのパターンマッチ
左辺に、
#<レコード名>{<要素名1>=値,<要素名2>=変数名,..}
と言った形でパターンマッチできます。
2> P = #item{id=1,name="test",price=100}.
3> #item{id=1} = P.
#item{id = 1,name = "test",price = 100}
4> #item{id=1,name=Name} = P.
#item{id = 1,name = "test",price = 100}
5> Name.
"test"
リスト
[<要素1>,<要素2>...]
1> [1,2,3].
[1,2,3]
これはLispなどのリストと同じリストです。内部的にはLispと同様cons
セルによる再帰構造で最後がnil
となっています。この事は後述します。
リスト処理
リストはパターンマッチで処理が可能です。
次にリストの長さを計算する関数を例として載せます。
len([]) ->
0;
len([Head|Tail]) ->
1+len(Tail).
[]
は空のリストにマッチします。
[Head|Tail]
のHead
はリスト先頭、たとえば[1,2,3]
とあったら1
にマッチしてTail
は[2,3]
にマッチします。
リストを処理する関数は標準ライブラリのlists
というモジュールにまとめられています。
タプル
{<要素1>,<要素2>...}
1> {1,2,3}.
{1,2,3}
タプルはErlangではLispのS式代わりに使われることが多いです。
バイナリ
<< <値1>:<サイズ1>/<タイプ>, <値2>:<サイズ2>/<タイプ> ...>>
というふうにかいて値をサイズ毎に指定できます。サイズ指定を省略するとデフォルトの8ビットになります。
このサイズの大きさごとに分割された塊をセグメントと呼びます。
タイプは型、符号、エンディアンが指定でき、複数のタイプを-
で連結できます。
指定可能な型を以下に載せます。
型
-
integer
整数 -
float
浮動小数点 -
binary
バイナリ型 -
bytes
バイト型 -
bitstring
ビットストリング -
bits
ビット型 -
utf8
UTF-8のビットストリング -
utf16
UTF16のビットストリング -
utf32
UTF32のビットストリング
符号
-
signed
符号付き -
unsigned
符号なし
エンディアン
-
big
ビッグエンディアン -
little
リトルエンディアン -
native
CPUネイティブの エンディアン -
unit
セグメントのサイズ
バイナリ演算子
-
bsl
左シフト -
bsr
右シフト -
band
bit論理積 -
bor
bit論理和 -
bxor
bit排他的論理和 -
bnot
bit論理否定
バイナリのパターンマッチ
バイナリのパターンマッチの例としてRGBカラーの例を載せます。
1> Color = <<16#FF00FF:(8*3)>>.
<<255,0,255>>
としてRGBのそれぞれの8ビットを,
で区切って定義することができます。
2> <<R,G,B>> = Color.
<<255,0,255>>
この様にパターンマッチによって8ビットごとに分解したりすることができます。
先頭のRだけほしい場合は、ワイルドカードを使って次の様に書くことで取得できます。
2> <<R:8,_/binary>> = Color.
<<255,0,255>>
3> R.
255
余談ですが、Erlangで日本語のような多バイト文字を扱うには
io:format("~ts~n",[<<"お"/utf8>>]).
のようにバイナリ形式でUTF-8を指定する必要があります。詳しくはErlangのドキュメントをご覧ください。2
内包表記
内包表記にはリスト内包表記とバイナリを内包表記の二種類があります。
もともとは、内包表記とは集合論において条件Pに合致する要素を集めた集合を表現する記法です。
これを表現しようとした記法がMirandaに取り入られました。この記法は後にHaskellや[Pythonにも取り入れられています。
リスト内包表記
erl
に
Erlang R16B03 (erts-5.10.4) [source] [64-bit] [async-threads:10] [kernel-poll:false]
Eshell V5.10.4 (abort with ^G)
1> [ X*2 || X<-[1,2,3,4,5], X<4 ].
と打ち込んでみてください。
すると、
[2,4,6]
帰ってくるはずです。
これは、X<-[1,2,3,4,5]
によって、リストが生成されます。
次に、条件X<4
に合致する1,2,3
がフィルタリングされ、
最後に式X*2
によって1*2,2*2,3*2
のリストが作られたということです。
一般化すると次のようになります。
[式 || パターン <- リスト, 条件]
バイナリ内包表記
バイナリ内包表記は内包表記のバイナリ版です。
<<式 || パターン <= バイナリ, 条件>>
並列
Erlangにおいて並列処理は、軽量プロセス(以下プロセス)とメッセージによって実現されています。
ErlangのプロセスはOSのプロセスに似ていますが、ErlangのVMで実行されるのでOS内部でのコンテキストスイッチが行われません。
Erlangのプロセス同士はメモリ領域を共有せずにメッセージ・キューを通じて通信を行います。
Erlangのプロセスはレジスタの一時退避やカーネルモードへの切り替えなどの複雑なコンテキストスイッチを伴わないため切り替えは高速です。
アクターモデル
Erlangの並列モデルであるアクターモデルにについて簡単に紹介します。
メッセージを送受信できるアクターと呼ばれるオブジェクトが、メッセージを非同期に送受信することによって、
並列的に計算処理を行う計算モデルです。Erlangにおいてはプロセスがアクターに相当し、お互いにメッセージを送受信しています。
このアクターモデルの概念はErlang以外にもScalaのフレームワーク等に採用されています。
spawn
spawn
は次の様な形で呼び出して軽量プロセスを生成します。
Pid = spawn(<モジュール名>, <関数名>, <引数のリスト>)
spawn
は引数の関数からプロセスを生成し、プロセスIDと呼ばれるプロセス毎にユニークな数値を返します。
プロセスIDとプロセスは紐付いていて、プロセス間におけるメッセージの送受信の際にこのプロセスIDを使用します。
メッセージの送信
メッセージの送信は次の様に送信したいプロセスのプロセスIDに!
メッセージという形式でメッセージを送信します。
Pid ! メッセージ
メッセージの受信
次の様にreceive
でメッセージを受信し、パターンマッチでメッセージに対応する処理を行うのが一般的です。
receive
<パターン1> ->
<処理1>;
<パターン2> ->
<処理2>;
............
<パターンN> ->
<処理N>
after n ->
<タイムアウト処理>
end
after
はnミリ秒たった時のタイムアウト時の処理をします。
プロセス登録
spawn
が生成するプロセスIDをいちいちプログラマが変数で保存しておくのは面倒です。特に複数のプロセスがメッセージを送るプログラムを作るときは管理がとても面倒になります。
ErlangにはプロセスIDではなく、アトムを指定してメッセージを送る方法があります。
それがプロセス登録です。プロセスを登録するにはregister
関数を使用します。
本書の範囲を超えるので詳しくは述べませんが、プロセス登録機能は分散環境ではよく使用します。
register(Atom,Pid)
とすることで、
Atom
!
メッセージ
と言った形でアトムを使って登録したプロセスにメッセージが送信できます。
spawn_link
spawn_link
は基本的にspawn
と同じですが、例外が発生した場合の動作が異なります。
生成した子プロセスで発生した例外をプロセスが作成した側の親プロセスも受け取ります。
spawn
の場合、親プロセスは例外を受け取りません。
例外
例外は次の様にして書きます。
try <評価される式> of
<パターン1> ガード1 -> <処理1>;
<パターン2> ガード2 -> <処理2>
catch
<例外のタイプ1>: <パターン1> ガード1 -> <例外処理1>;
<例外のタイプ2>: <パターン2> ガード2 -> <例外処理2>
after
<例外が起きても起きなくても実行される処理>
end
try ... of
で囲まれた式において例外が発生するとcatch
以降でパターンマッチが行われ、例外処理が実行されたあとにafter
の処理が行われます。
次は例外を発生させる関数です。
throw
次の様にして例外を発生させることができます。
throw(<式>)
exit
次の様にして例外を発生を起こし、プロセスを終了させます。
exit(<式>)
error
次の様にして重大な例外を発生させ、プロセスを終了させます。
error
は主にEralngのVMが生成するランタイムエラーとして使用されます。
error(<式>)
モジュール
モジュールはソースを記述するファイル単位でコンパイルする際の最小単位です。ソースは何らかのモジュールに属していないとコンパイルできません。
モジュールを作成するには.erl
ファイルに
モジュール宣言などのモジュール属性を記述する必要があります。
モジュール属性
モジュール属性はファイルの先頭によくある-
から始まる宣言です。
たとえば、モジュール宣言などがあります。
モジュール宣言
-module(<モジュール名>).
エクスポート宣言
エクスポート宣言する関数を宣言します。
次の様に関数名と引数の数を/
でつなげてください。
-export([<関数名>/<引数の数>])
次の様にすればモジュールで作成した全ての関数をエクスポートできます。
-export_all
コンパイルオプション宣言
コンパイラに指定するオプションです。3
マクロ定義
次の様にしてマクロを定義できます。
-define{<マクロ>,<式>}
レコード宣言
次の様にしてレコードを宣言できます。
-record(<レコード名>, {<要素名1>,<要素名2>,...}).
インクルード
次の様にしてヘッダファイルをインクルードできます。
-include("<ヘッダファイル>").
ヘッダファイルの拡張子は.hrl
です。