Erlang 基礎ポイント2 - プログラムの形式, 無名関数, パターン照合など

Erlangプログラムの形式

REPL環境でなく、ソースファイル(erlファイル)から実行するためには
以下の形式に従う。

-module(test).
-export([func1/0,func3/1,func4/3]).
func1() -> func2() + 5.
func2() -> 3.
func3(X) ->
    Y = X * 2,
    Y * 2.
func4(plus, X, Y) -> X + Y;
func4(minus, X, Y) -> X - Y;
func4(_, _, _) -> "nothing".
% コメント

まず、module文にモジュール名を書く。
モジュール名はソースコードを含んでいるファイル名(拡張子はerl)にする。
モジュールはErlangコードの基本単位だ。

export文には外部からアクセスを許容する関数名・引数の数を記述する。
Javaにおけるpublicといえば、分かりやすい。
コンマで区切って複数指定可能。

次はJavaのようにメンバー変数が来そうだが、そんなものはない。
すぐ処理ロジックを含む関数が来る。
関数のシグネチャにリターンタイプとか、引数の型の指定がない。
関数名と引数名だけを書いて、->の後、すぐ処理内容を記述する。
関数の処理内容は行ごとにコンマで区切って、関数の最後にピリオドを打つ。

引数の数が同じ関数を複数(いわゆる、オーバーロード)書く場合、
各関数の終わりにコンマでなくセミコロンを書く。
但し、最後の関数の終わりにはコンマを書く。

型のチェックは実行時に行われる。
つまり、Erlangは動的型付け言語である。
コメントは%の後に書く。

無名関数

Erlangでは関数を無名で宣言して、変数に代入したり関数の引数や戻り値として使用することができる。

1> F = fun(A) -> A * A end.
#Fun<erl_eval.6.80247286>
2> F(2).
4
3> Y = F.
#Fun<erl_eval.6.80247286>
4> Y(4).
16

funで始まって、end.(ピリオドがあることに注意)で終わる。
次は無名関数を関数の引数に渡したり、戻り値として返す例だ。

% 引数
9> F = fun(A) -> A * A end.   
#Fun<erl_eval.6.80247286>
10> lists:map(F, [1, 2, 3, 4]).
[1,4,9,16]
% 戻り値
12> Plus = fun(X) -> (fun(Y) -> X + Y end) end.
#Fun<erl_eval.6.80247286>
13> PlusFive = Plus(5).
#Fun<erl_eval.6.80247286>
14> PlusFive(9).
14

リスト内包表記

リストから特定条件を満たす要素で構成された新しいリストを作るためのErlangの機能。
普通、リストから特定要素を持つ新しいリストを作るためには
新しい空のリストを宣言しておいて、ループと条件文を利用し、
条件にマッチした要素を空のリストに入れる処理が必要なので、
コードが長くて読みづらくなる。

しかし、リスト内包表記を使うと、リスト(例 : [ 1, 2, 3])の中で
新しいリストを作るための専用の表記ができるので、コードが短くて
読みやすい。

1> L = [1, 2, 3].
[1, 2, 3]
2> [X - 1 || X <- L].
[0, 1, 2].

後ろの部分、X <- LはリストLから要素Xを取り出すという意味で、
"||"の左側に要素Xで行う式を書く。

レコード

Cにおける構造体のようなもの。
Erlangでレコードの宣言方法は色々あるが、よく使われる方法は
拡張子がhrlのファイルにレコードを定義し、
そのhrlファイルをerlファイルで
インクルードする方法だ。

% person.hrl
-record(person, {
  name,
  age,
  tel = 000-0000-0000
}).
% test.erl
-module(test).
-export([duke/0]).
-include("person.hrl"). #インクルード

duke() ->
  #person{name=duke, age=34, tel="0800000-0000"}.

レコードの宣言は、まず、-recordキーワードを書いて、
1番目の引数にレコード名、次に要素名のタプルを指定する。
要素にはデフォルト値の指定ができる。

レコードの使用は

    #レコード名{要素1=値, ... , 要素x=値}

のようにする。

パターン照合

Erlang 基礎ポイント1 - DukeLabでちょっとだけ、触れたが、ここでもうちょっと説明しよう。
Erlangで、=は代入するのではなく、パターン照合するという。

以下の場合、Aに100を代入するのではなく、右側(100)の評価結果と左側のパターン(A)を照合するのだ。

 A = 100

上記の例だと、ぱっとイメージしにくいだろう。
パターン照合とイメージが似ているのは正規表現ではないかな。
正規表現を使うと、ある複雑な文字列からパターンにマッチする特定の部分を手軽に抽出することができる。
ただし、正規表現のルールが分かりにくい短所はある。

同じくErlangのパターン照合も正規表現のようなものだが、
文字列だけでなく、タプル、リスト、関数の引数など、何にでも使えると考えればいいだろう。

以下の例を見てみよう。

19> T = {{name, "duke"}, {age, 34}}.
{{name,"duke"},{age,34}}
20> {{_,_},{_,MyAge}} = T.    
{{name,"duke"},{age,34}}
21> MyAge.
34

Tにタプルが二つ({name, "duke"}と{age, 34})が入っているが、
二つ目のタプルから年齢(34)を取得したい場合、20>のように
パターン照合すればいい。20>の左側にあるパターンのうち、
_(アンダーバー)はダミー要素で何でもマッチする。
マッチするだけで変数に束縛したくない場合、_(アンダーバー)を使う。
パターンのうち、大文字で始まっているMyAgeが
二つめのタプルにある2番めの要素(34)に一致して
年齢(34)を取り出せたことが分かる。
パターン照合の基本が分かれば、後はマニュアルか
ググってパターン照合の例を参考すればいいだろう。

最後に関数のパターン照合に関する例を載せる。

% func_test.erl
-module(func_test).
-export([add/2]).
add(X,plus) -> X + X;
add(X,pow) -> X * X;
add(X,Y) -> X + Y + 1.

% 以下はREPL環境で実行したもの。
1> c(func_test).
{ok,func_test}
2> func_test:add(3, plus).
6
3> func_test:add(3, pow). 
9
4> func_test:add(3, 5).  
9
5> c(func_test).          
{ok,func_test}

ガード

ある関数を呼び出すと、呼び出される関数を探すため、
引数に指定した値の型と関数のシグネチャとのパターン照合を行う。
Javaオーバーロードと似ているものだと考えればよさそう。

追加的にメソッド内で引数の値の内容によって、違う動作をする必要がある時、
Javaではif文やポリモーフィズムを使うが、Erlangではガードを使えばいい

ここでは関数を中心にガードを使い方を説明するが、
case式やif式でも使用できる。

以下のソースを見てみよう。

% guard_module.erl
-module(guard_module).
-export([guard_test/1]).

guard_test(X) when X < 5, X > 1 -> X * X;
guard_test(X) when X >= 5; X < 10 -> X - 2;
guard_test(X) -> X - 2.

上記のソースを解析してみると、
呼び出された関数がguard_testで一番目の引数(X)に値が指定されていて、
Xが5未満かつ1より大きければ(コンマ[,]はandを意味)、X * Xを、
Xが5以上か10未満なら(セミコロン[;]はorを意味)、X - 2を、
それ以外の場合(つまり、elseなら)、X - 2
を返すという意味だ。

以下は上記の関数をREPL環境でテストしたものだ。

1> c(guard_module).
{ok,guard_module}
2> guard_module:guard_test(5).
3
3> guard_module:guard_test(1).
-1
4> guard_module:guard_test(6).
4
5> guard_module:guard_test(15).
13
6> 

ガードは条件文のようなものだと考えればいい。

ガードは便利そうだが、関数の宣言部が複雑になる恐れがあるので、
なるべくパターン照合だけで済ませた方がいいと思う。

今日はここまで。