5月2日、Agis Anastasopoulos氏が「Dabbling in Erlang, part 2: A minimal introduction」と題した記事を公開した。
Erlangのパターンマッチングを使えば、従来のif-else文やswitch文を書く必要がほとんどなくなる。関数の引数部分で直接条件分岐を記述でき、コンパイラが自動的に効率的な実行コードを生成してくれるからだ。近年、マルチコア活用や分散システム構築の需要が高まる中で、並行処理に特化したErlangへの注目が再び集まっている。本記事では、そんなErlangの中核機能であるパターンマッチングの基本から実践的な使い方まで、豊富なサンプルコードとともに解説する。
パターンマッチングによる変数束縛
Erlangにおける変数は、数学的な意味での変数として扱われる。つまり、一度値が束縛された変数は、その値を変更できない(イミュータブル)。
Eshell V5.10.1 (abort with ^G)
> X = 5.
5
> X = 6.
** exception error: no match of right hand side value 6
> X = 5.
5
重要なのは、Erlangの=演算子は代入ではなくパターンマッチングを行うという点だ。これにより、複雑なデータ構造から値を一度に抽出できる。
> {_, Name, Surname} = {person, "John", "Doe"}.
{person,"John","Doe"}
> Name.
"John"
> Surname.
"Doe"
_(アンダースコア)は「どんな値でも受け入れるが、使わない」ことを表すワイルドカードだ。
if文が不要になる関数定義
パターンマッチングの真価は、プログラムの実行フローを制御できる点にある。挨拶の種類に応じて異なるレスポンスを返す関数を例に見てみよう。
他の言語(例:Ruby)では以下のようになる:
def greet(inc)
case inc
when "Hello" then "Hi"
when "Goodbye" then "See you"
else inc
end
end
Erlangでは、関数の引数部分で直接パターンマッチングができる:
greet("Hello") -> "Hi";
greet("See you") -> "See you";
greet(Other) -> Other.
この書き方により、case文やif文を使わずに済む。一つの関数は複数の節(clause)の集合として定義され、コンパイラが効率的な二分探索木を生成するため、パターンマッチングは非常に高速だ。
より複雑な例:図形の面積計算
図形の面積を計算する例で、パターンマッチングの威力をさらに見てみよう:
area({circle, Radius}) -> 3.14 * Radius * Radius;
area({square, Side}) -> Side * Side.
使用例:
> area({square, 15}).
225
> area({circle, 5}).
78.5
> area({wtf, 5}).
** exception error: no function clause matching area({wtf,5})
明示的にエラーハンドリングを書く必要がない。パターンマッチングが自動的に例外を発生させてくれる。これにより、想定外の入力に対する堅牢性が自然に確保される。
ガードによる条件追加
ガードは、パターンマッチングに追加の制約を加える仕組みだ:
is_what(X) when X rem 2 == 0 -> even;
is_what(X) when X rem 2 /= 0 -> odd.
whenキーワードに続く条件式がガードとなる。これにより、値の内容だけでなく、その値が満たすべき条件も同時に指定できる。
リストと関数型プログラミング
Erlangにおいてリストは最も重要なデータ構造だ。リストの先頭要素(head)と残り(tail)は、cons演算子(|)とパターンマッチングで簡単に取り出せる:
> [Head|Tail] = [1,2,3,4,5].
[1,2,3,4,5]
> Head.
1
> Tail.
[2,3,4,5]
Erlangでは関数が第一級オブジェクトとして扱われる。独自のmap/2関数を実装してみよう:
map(F, []) -> [];
map(F, [H|T]) -> [F(H) | map(F, T)].
使用例:
> map(fun(X) -> X*2 end, [1,2,3]).
[2,4,6]
たった2行で、リストの各要素に関数を適用する高階関数を実装できた。パターンマッチングにより、空リストと非空リストの場合分けが自然に表現されている。
リスト内包表記による簡潔な記述
リストの変換とフィルタリングを組み合わせる作業は頻繁に発生する。リスト内包表記により、この処理を簡潔に記述できる:
> [X*2 || X <- [1,2,3,4,5], X rem 2 == 0].
[4,8]
この例では、リストから偶数のみを選び出し、それぞれを2倍している。より複雑な例:
> [{X,Y} || X <- lists:seq(1,4), X rem 2 == 0, Y <- lists:seq(X,4)].
[{2,2},{2,3},{2,4},{4,4}]
複数のジェネレータと条件を組み合わせることで、強力なデータ変換が可能になる。
実装時の重要な注意点
パターンマッチングの節の順序は重要だ。上から順に評価されるため、より具体的なパターンを先に配置する必要がある。以下の例は期待通りに動作しない:
area(Other) -> {unknown, Other}; % これが最初にあると
area({circle, Radius}) -> 3.14 * Radius * Radius; % 到達されない
area({square, Side}) -> Side * Side.
正しくは、具体的なパターンを先に書く:
area({circle, Radius}) -> 3.14 * Radius * Radius;
area({square, Side}) -> Side * Side;
area(Other) -> {unknown, Other}. % 最後に配置
Erlangが注目される理由
こうしたパターンマッチングは、Erlangの並行処理機能と組み合わさることで真価を発揮する。Erlangは元々電話交換システムのために設計された言語で、数百万の軽量プロセスを効率的に管理できる。現代のマルチコアCPUや分散システムにおいて、この特徴が再び脚光を浴びている。WhatsAppが22人のエンジニアで4億5000万ユーザーを支えられたのも、Erlangの並行処理能力があってこそだった。
著者は次回の記事でErlangの並行性機能について解説する予定だという。パターンマッチングという基礎を理解した今なら、Erlangの真の力である並行処理もより深く理解できるだろう。
関連リソース:
詳細はDabbling in Erlang, part 2: A minimal introductionを参照していただきたい。