Rob Phoenixさんの2016年1月4日付のブログ記事Notes on Elixir: Pattern-Matching Mapsの翻訳です。ここのところ立て続けにElixirについてのノートを投稿していて前回の投稿は私の前回の翻訳とほぼ同じ内容でした。つまり単純なスカラー型データやリストについての例です。
今回取り挙げられているのはMap型のデータでElixirのパターンマッチングがどうなるか、という例です。
パターンマッチングについての前回の投稿からの続きで、Elixirで主なキー・バリューストアとして使われるmap
型データは、パターンマッチングについては他のデータ構造と全然違う面白い機能を持っています。
map
はその値のまさにサブセットだけをパターンマッチさせることができます。マッチングのパターン内のキーはマッチさせる対象の中に存在している必要がありますが、list
やtuple
の場合のようにマッチング対象とパターンが正確に一致している必要はありません。例えば:
iex(1)> [a, b] = [1, 2, 3]
** (MatchError) no match of right hand side value: [1, 2, 3]
iex(1)> {a, b} = {1, 2, 3}
** (MatchError) no match of right hand side value: {1, 2, 3}
iex(1)> %{:a => one} = %{:a => 1, :b => 2, :c =>3}
%{a: 1, b: 2, c: 3}
iex(2)> one
1
iex(3)> %{:a => one, :c => three} = %{:a => 1, :b => 2, :c =>3}
%{a: 1, b: 2, c: 3}
iex(4)> three
3
iex(5)> one
1
iex(6)> %{} = %{:a => 1, :b => 2, :c =>3}
%{a: 1, b: 2, c: 3}
iex(7)> %{:d => four} = %{:a => 1, :b => 2, :c =>3}
** (MatchError) no match of right hand side value: %{a: 1, b: 2, c: 3}
iex(8)> %{:a => one, :d => four} = %{:a => 1, :b => 2, :c =>3}
** (MatchError) no match of right hand side value: %{a: 1, b: 2, c: 3}
list
もtuple
もパターンのデータ構造とマッチしようとするデータ構造、端的には大きさ、が異なっているとマッチングに失敗するのがわかりますね1。ところがmap
では事情が異なります。キーがパターンと、マッチしようとするデータの両方に含まれているならば、それらの大きさが異なっていてもマッチングは成功します。空のmap
であっても2です。
しかしながらパターンに入っているキーがマッチしようとするデータに入っていない場合はマッチングは失敗します。これはたとえ他のキーがマッチした場合であってもです。従ってパターンで使われる全てのキーはマッチしようとするデータに含まれている必要があります。
この仕組みはPhoenix Frameworkの中でパラメータを関数に引き渡そうとする際に広く使われています。関数は必要とするデータだけを取り出すことができるのです。
新規ユーザーを生成するための関数はこうなります:
def create(conn, %{"user" => user_params}) do
# 変数user_paramを使って何かする
end
この関数の第2引数として渡されるmapの中身は実際にはこのようになっています:
Parameters: %{"_csrf_token" => "UR99GwILHTtzbSBUYRwmBVpdeDY/AAAA3K70jEiO9UhgPVwh+d3WYw==",
"_utf8" => "✓",
"user" => %{"name" => "Memphis Minnie",
"password" => "[FILTERED]",
"username" => "minnie"}}
create
関数は"user"
キーに対してのみパターンマッチングを行い、対応する値、この場合はまた別のmapですが、をこの関数内で使用される変数user_params
にバインドします。この関数は基本的に与えられたパラメータのmapからユーザー情報だけを取り出し、必要としているものと関係ない残りのデータは捨ててしまいます。
map全体をマッチしつつ同時にパターンマッチされたmapの部分をマッチすることも可能です。create
関数の中身を書き換えてみましょう:
def create(conn, parameters = %{"user" => user_params}) do
IO.inspect user_params["name"]
IO.inspect parameters["_utf8"]
# 変数user_paramを使って何かする
end
この関数を使ってparameter
と渡されたパラメータ全体のmapをマッチさせ、user_params
をパラメータ全体mapの"user"
キーとマッチさせることで以下のように表示されるはずです。
[info] POST /users
"Skip James"
"✓"
面白いことにparameters
のパターンマッチングは他の書き方もできます:
def create(conn, %{"user" => user_params} = parameters) do
IO.inspect user_params["name"]
IO.inspect parameters["_utf8"]
# 変数user_paramを使って何かする
end
なぜならここでは%{"user" => user_params} = parameters
がマッチのパターンであり、マッチさせようとするデータが渡されたパラメータ全体のmapだからです。さらにパターンの内側で異なる部分をパターンとマッチさせて、他の変数にバインドすることもできます。私の知る限りこれはより慣用句的に使われている手法で、ほとんどの場合こちらが見られるでしょう。
パターンマッチングはmap
キーバリューストアから値を取り出すのに実に優れた方法を提供してくれます。