スキップしてメイン コンテンツに移動

Erlang基礎文法最速マスター

先日、Lua基礎文法最速マスターを書いてみたが、予想以上に自分自身への学習効果が高かった。そこで、普段使っているPythonと同じぐらいに使いこなしたいと思っていたErlang(アーラン)の基礎文法最速マスターを書いてみることにした。

Erlangが関数型プログラミング言語であることもあり、書き下すのは思ったよりも大変だったが、学習効果はかなりあったと思う。言語を習得したいときはこのようなまとめを書いてみるのが良さそうだ。

ただ、手続き型プログラミング言語に比べて異なる部分があまりにも大きいので、すべてを説明することは難しく、これを読んだだけですぐに使えたりはしないかもしれない。また、間違っていたり、足りない部分などがあったら、教えて頂けるとありがたい。


1. 基礎

対話環境

コマンドラインからerlを実行すると対話環境(シェル)になります。コマンドの最後にはピリオド(.)が必要です。help()で簡単な説明を読むことができます。

$ erl 1> A = 10. % Aに10を束縛. 10 2> B = A * 5. % BにA * 5 (50)を束縛. 50 3> hello. % helloはアトム(定数のようなもの/後述). hello 4> b(). % 現在の束縛変数を表示. A = 10 B = 50 ok 5> A = 20. % 変数は一度しか代入(束縛)できない. ** exception error: no match of right hand side value 20 6> f(). % 束縛変数をすべてクリアする. ok 7> A = 20. % 再度束縛できる. 20 8> q(). % Erlangシェルを終了させる. ok

画面出力

io:formatで書式指定の出力を行うことができます。

~p : 整形して出力. ~s : 文字列を出力. ~f : 浮動小数点数を出力. ~w : Erlangの標準構文として出力(アトムならそのまま、文字列ならリストとして出力など) ~n : 改行.

io:format("Hello world~n"). % "Hello world" %% "5 , 1.235, 30, erlang" io:format("~-5w, ~6.3f, ~p, ~s~n", [5, 1.23456, 30, "erlang"]).

コメント

%から行末までがコメントになります。ブロックコメントはありません。慣例として%%がよく使われます。

% コメント. %% コメント.

変数の代入

Erlangの変数の先頭は必ずアルファベットの大文字かアンダーバー(_)になります。また、一度代入すると後から変更できません。代入した変数のことを束縛変数と呼びます。まだ代入されていない変数のことを未束縛変数と呼びます。変数は動的型付けなので宣言は必要ありません。

A = 5. % ※1 B = "Hello world". C = atom. A = 10. % Aは束縛変数のためエラーになる. A = 5. % ※2 Aは束縛変数だが、パターン照合が一致するのでエラーにならない.

実のところ、=は代入演算子ではなく、パターン照合演算子になります。例えば、※1のA = 5のAは未束縛変数であり、どんな値にもパターンが一致するので5の照合が成功して、Aに5が束縛できるのです。※2のA = 5では、既にAは5に束縛されているので、数値の5か、5の値を持つ変数とだけ照合が成功します。

Erlangで重要な要素の一つにアトムがあります。アトムとは数値以外の不変値を表すのに使用します。C言語の列挙子などが近い存在です。アトムは通常アルファベットの小文字から始まります。変数に代入することができます。

atom. A = hello.

また、先頭文字を大文字にしたり、空白や特殊記号を使用したい場合はシングルクォーテーション(')で囲むことでアトムとなります。

'Atom'. 'This is an atom.'.

ある項目を一つにまとめたい場合はタプルを使います。タプルは中括弧({})で括ることにより作れます。

A = {pen, book, desk}. B = {b, {language, "erlang"}, 10}.

タプルから値を取り出す場合、パターン照合を使います。

C = A. % 未束縛変数Cに {pen, book, desk}が入る. {b, D, 10} = B. % Bのタプルと未束縛変数Dを含む{b, D, 10}はパターンが一致するのでDに{language, "erlang"}が入る. {b, E, 20} = B. % パターンが一致しないのでエラーになり、Eは未束縛変数のまま.

いくつかのパターン照合の例を以下に挙げておきます。

{X, 123} = {10, 123}. % 成功. Xに10が入る. {X, 12} = {10, 123}. % 失敗. 12と123が一致しない. {A, B, A} = {5, 10, 5}. % 成功. Aに5、Bに10が入る. {A, B, C} = {5, 20, 8}. % 失敗. Bには10が束縛されている. {A, _, _} = {5, 20, 8}. % 成功. アンダーバー(_)はどの値とも一致する. [H|T] = [1, 2, 3]. % 成功. Hに1、Tに[2, 3]が入る. [A, B|T] = [1, 2, 3, 4, 5]. % 成功. Aに1、Bに2、Tに[3, 4, 5]が入る.

スクリプトの実行

コンパイルして実行するには以下のようにコマンドを実行します。ただし、ソースコードをコンパイルするためには、先頭でモジュール名をアトムで指定する必要があり、モジュール名は基本的に拡張子を除いたファイル名が使われます。例えば、ソースコードのファイル名がtest.erlであれば、-module(test).と指定します。-exportについては後述の関数の説明を参照してください。

test.erl

-module(test). -export([hello/0]). hello() -> io:format("Hello world~n").

シェル上で実行:

1> c(test). % コンパイル. {ok,test} 2> test:hello(). Hello world ok

コマンドラインで実行:

$ erlc test.erl % コンパイル. $ erl -noshell -s test hello -s init stop Hello world

ここでは、-noshellにより対話型シェルを立ち上げず、-s test helloでtest:hello()を実行し、-s init stopで終了させています。

次に示すように、escriptで実行すればコンパイルは不要ですが、実行時に呼ばれる関数はmain/1となります。-moduleと-exportは必要ありません。

test.erl

#!/usr/bin/env escript main(_) -> io:format("Hello world~n").

実行:

$ ./test.erl Hello world

コマンドライン引数を取る場合は、以下のようにします。

test.erl

-module(test). -export([main/0, main/1]). main() -> % 引数なしの場合. io:format("No argument.~n"). main(A) -> % 引数ありの場合. [io:format("~s ", [X]) || X <- A], io:nl().

実行:

$ erlc test.erl % コンパイル. $ erl -noshell -s test main Hello world -s init stop Hello world $ erl -noshell -s test main -s init stop No argument.

2. 数値

数値の表現

Erlangは最後がピリオド(.)になるので、1.は整数の1であり、実数の1.0ではありません。実数の1.0にするには、1.0.と書く必要があります。

A = 1. % これは整数の1 B = 1.0. % これは実数の1.0

各種演算

5 + 2. % 7 5 - 2. % 3 5 * 2. % 10 5 / 2. % 2.5 5 div 2. % 2 (商) 5 rem 2. % 1 (剰余)

インクリメントとデクリメント

Erlangは関数型言語で代入は一度しかできないので当然インクリメントはできません。

3. リスト

Erlangでは角括弧によりリストを作ることができます。また、単一代入なので一度作成したリストを変更することはできません。変更したい場合は別のリストを作成します。

リストの作成

A = [1, 2, 3].

リストの要素への参照

Erlangではリストの先頭のインデックスは1となります。

%% 要素の参照. lists:nth(1, A). % リストAの先頭の要素. hd(A)と同じ. lists:nth(2, A). % リストAの2番目の要素.

リストの個数

N = length([1, 2, 3]). % 3

リストの操作

A = [1, 2, 3]. %% 先頭を取り出す. F = hd(A). % Fは 1 [H|T] = A. % Hは 1、Tは[2, 3] %% 先頭に追加. B = [5|T]. % Bは[5, 2, 3] %% 末尾を取り出す. C = lists:last(B) % Cは3 [L|R] = lists:reverse(B). % Lは3、Rは[2, 5] %% 末尾に追加. D = A ++ [9]. % Dは[1, 2, 3, 9]

4. 文字列

文字列表現

ダブルクォーテーション(")で括ると文字列になりますが、Erlangではそれを整数のリストとして扱っています。

"abc". % [97, 98, 99] と同じ意味. io:format("~p~n", [[97, 98, 99]]). % "abc"と表示される. A = "abc". % [97, 98, 99]

文字列操作

%% 結合. A = string:concat("aaa", "bbb"). % "aaabbb" A = lists:concat(["aaa", "bbb", "ccc"]). % "aaabbbccc" join([]) -> []; join([H|T]) -> string:concat(H, join(T)). % バイナリ文字列の場合は、Hをbitstring_to_list(H)に. A = join(["aaa", "bbb", "ccc"]). % "aaabbbccc" %% 分割. A = re:split("aaa, bbb, ccc", "\s*,\s*"). % [<<"aaa">>, <<"bbb">>, <<"ccc">>] バイナリ文字列. A = regexp:split("aaa, bbb, ccc", "\s*,\s*"). % ["aaa", "bbb", "ccc"] regexpは遅いのでre推奨. %% 長さ. L = length("abc"). % 3 %% 切り出し. S = string.substr("abcd", 1, 2). % "ab" 文字列の先頭は1、先頭から2文字を切り出し. %% 検索. R = string:rstr("abcd", "cd"). % 見つかった場合はその位置(この場合は3)、見つからなかった場合は0が返る.

比較演算子

整数と浮動小数点数の比較以外では==と/=の代わりに、=:=と=/=を使うようにしましょう。

N1 == N2 % N1はN2と等しい. N1 =:= N2 % N1はN2と同一. N1 /= N2 % N1はN2と等しくない. N1 =/= N2 % N1はN2と同一でない. N1 < N2 % N1はN2より小さい. N1 > N2 % N1はN2より大きい. N1 =< N2 % N1はN2以下. N1 >= N2 % N1はN2以上.

5. 関数

Erlangでは関数名は必ずアルファベットの小文字から始まり、以下のような構文になります。

関数名(引数) [when ガード] -> 式1, 式2, ... 式N.

式はカンマ(,)で区切り、関数の終端はピリオド(.)になります。また、モジュールの外部から関数を呼び出すためには-exportアノテーションで関数を指定しなければなりません。例えばmain([A])を呼び出したい場合、-export([main/1]).のように指定します。これは引数が1つあるmain関数をエクスポートするという意味です。Erlangでは引数の個数のことをアリティと呼びます。因みに、-exportで関数を指定しなくても、-compile(export_all).とすれば、モジュール内の関数すべてを外部から呼び出せます。

以下のsum関数は再帰を使ってリスト内のすべての要素を足し合わせます。ここでは引数のパターンが[]と[H|T]の2種類出てきていますが、上から順にパターンが照合され一致する関数が評価されます。それぞれの関数の区切りにはセミコロン(;)が使用されます。

sum([]) -> 0; sum([H|T]) -> H + sum(T).

関数名の右側にあるwhenとはガードと呼ばれるパターンを補助する仕掛けで、このガードの条件を満たさなければパターンが一致していても式は評価されません。以下のsum_odd関数はカードを使って奇数のみを足し合わせています。

sum_odd([]) -> 0; sum_odd([H|T]) when H rem 2 =:= 1 -> H + sum_odd(T); sum_odd([_|T]) -> sum_odd(T).

サンプルプログラムとして、ガードを使ってFizzBuzz問題を解いてみます。1から100までを表示する場合はfizzbuzz(100).と実行します。

fizzbuzz(N) when N rem 15 =:= 0 -> io:format("FizzBuzz~n"); fizzbuzz(N) when N rem 3 =:= 0 -> io:format("Fizz~n"); fizzbuzz(N) when N rem 5 =:= 0 -> io:format("Buzz~n"); fizzbuzz(N) -> io:format("~p~n", [N]).

パターン照合を使って以下のように書くこともできます。

fb(_, 0, 0) -> "FizzBuzz"; fb(_, 0, _) -> "Fizz"; fb(_, _, 0) -> "Buzz"; fb(N, _, _) -> N. fizzbuzz(0) -> ok; fizzbuzz(N) -> fizzbuzz(N-1), io:format("~p~n", [fb(N, N rem 3, N rem 5)]).

Erlangでは、funを使うことによって無名関数を使うことができます。

F = fun(X) -> X * 3 end. F(10). % 30

6. 制御文

case文

case 条件 of パターン1 [when ガード1] -> 式1; パターン2 [when ガード2] -> 式2; ... パターンN [when ガードN] ->式N end

パターンを1から順に条件と照合して一致するパターンの式を返します。case文でもガードを使うことができます。

以下に例を示します。

X = case {20, 30, 40} of {10, A, B} -> one; {20, A, B} when A > 50 -> two; {20, A, _} when A < 50 -> three; true -> others end.

タプル{20, 30, 40}とパターンを比べると、2番目のパターン{20, A, B}と一致してますが、ガードのA > 50の条件に一致しないので式は評価されません。次のパターン{20, A, _}も一致していて、さらにガードA < 50の条件とも一致するので式が評価され、アトムthreeがXに入ることになります。最後のパターンtrueはすべての条件と一致します。

上述のFizzBuzz問題をcase文とリスト内包表記(後述)を使って1行で書くこともできます。

[io:format("~p~n",[case{N,N rem 3,N rem 5}of{_,0,0}->fizzbuzz;{_,0,_}->fizz;{_,_,0}->buzz;{N,_,_}->N end])||N<-lists:seq(1,100)].

if 文

if ガード1 -> 式1; ガード2 -> 式2; ... ガードN -> 式N end

例を挙げます。

N = 10. %% Resultにはsameが入る. Result = if N < 10 -> low; N =:= 10 -> same; N > 10 -> high end.

for/while文

Erlangにはfor文もwhile文もありません。その代わりに関数を使って表現できます。

例えばC言語のfor文を使った以下のようなコードについて考えます。

for (i = 1; i <= 10; i++) { printf("%d\n", i * i); }

Erlangではこれを以下のように書くことができます。

for(M, M, F) -> [F(M)]; for(I, M, F) -> [F(I) | for(I+1, M, F)]. for(1, 10, fun(I) -> io:format("~p~n", [I*I]) end).

7. ファイル入出力

ファイル操作にはfileモジュールを利用します。

{ok, S} = file:open("input.txt", read). % 入力をinput.txtから読み込む. {ok, W} = file:open("output.txt", write). % 出力をoutput.txtに書き出す. %% input.txtから数値を読み込んで、その数値を倍にしてoutput.txtに書き込む. for_write(eof, _, _) -> ok; for_write(L, S, W) -> {N, _} = string:to_integer(L), io:format(W, "~p~n", [N*2]), for_write(io:get_line(S, ''), S, W). for_write(io:get_line(S, ''), S, W).

バイナリデータinput.datを読み込んで、それをoutput.datに2回書き込みます。

{ok, B} = file:read_file("input.dat"). file:write_file("output.dat", B). file:write_file("output.dat", B, [binary, append]).

8. 並行プログラミング

my_server.erlに以下のコードを作成します。

-module(my_server). -export([loop/0]). loop() -> receive hello -> io:format("Hello!~n"), loop(); bye -> io:format("See you!~n") end.

これをシェル上でspawn関数を使って、spawn(fun モジュール名:関数名/アリティ)のように呼び出します。因みにfun モジュール名:関数名/アリティは、関数の参照を行うための記法になります。

1> Pid = spawn(fun my_server:loop/0). 2> Pid ! hello. Hello! 3> Pid ! bye. See you!

spawn関数によりmy_server:loop関数をプロセス上に実行させて、プロセスIDのPidを取得し、送信演算子 ! でhelloメッセージを送ることによりプログラムから返事が返ってきます。上記のプログラムではbyeと送ることで終了します。

spawn関数は上記の方法のほかに、spawn(モジュール名, 関数名, [引数])のように使うこともできます。

1> Pid = spawn(my_server, loop, []).

並行プログラミングはErlangの存在を際立たせているのですが、ここで扱うには規模が大きすぎるので割愛します。詳しくは参考サイトなどを参照してください。

知っておいた方がよい文法

真偽値

真偽値の型はありませんが、アトムのtrueとfalseが真偽値のリテラルとして使われます。

not true. % falseアトムが返される.

論理演算子

true or false. % true true and false. % false not true. % false

ビット演算子

1 bor 2. % 3 5 band 3. % 1 5 bxor 3. % 6 bnot 3. % -4 1 bsl 2. % 4 - 左シフト 16 bsr 2. % 4 - 右シフト

map関数

%% [1, 4, 9, 16 ,25] - リストの要素をそれぞれ2乗にする. lists:map(fun(X) -> X * X end, [1, 2, 3, 4, 5]).

定義された関数を使う場合は以下のようになります。

square(X) -> X * X. % 2乗を返す関数. lists:map(fun square/1, [1, 2, 3, 4, 5]).

filter関数

%% [1, 3, 5] - 奇数のみのリストを作る. lists:filter(fun(X) -> X rem 2 =:= 1 end, [1, 2, 3, 4, 5]).

リスト内包表記

上述のmap関数はリスト内包表記を使って以下のように書くことができます。

[X * X || X <- [1, 2, 3, 4, 5]]. % [1, 4, 9, 16, 25]

上述のfilter関数は以下のように書くことができます。

[X || X <- [1, 2, 3, 4, 5], X rem 2 =:= 1]. % [1, 3, 5]

数値と文字列の変換

list_to_integer("12"). % 12 list_to_float("123.5"). % 123.5 integer_to_list(12). % "12" float_to_list(123.5). % "1.23500000000000000000e+002"

参考サイト


基礎文法最速マスターまとめサイト


コメント