Henrik Nyhさんの2016年1月9日付のブログ記事Pattern-matching complex stringsの翻訳です。前回と前々回でElixirのパターンマッチングについて見てきましたが、文字列ともパターンマッチできます。とはいえ、文字列とのパターンマッチングは制約が多いので…という例です。後半はパターンマッチング関係ないやん…という気もしますが…。
Elixirの文字列は他のバイナリーと同じように<>
結合演算子もしくは<<...>>
でビットパターンを指定することでパターンマッチングすることが可能です。
defmodule Example do
def run_command("say:" <> <<digit::bytes-size(1)>> <> ":" <> thing) do
count = String.to_integer(digit)
String.duplicate(thing, count)
end
def run_command("say:" <> thing), do: thing
end
Example.run_command("say:hi")
# => "hi"
Example.run_command("say:3:hi")
# => "hihihi"
しかしこれには大きな制約があります: 最後の(オプショナルな)一つを除いてバイナリーパターンの全ての部分は固定長でなければなりません。
そのためもっと複雑なパターンについては少々ワザが必要になります。例えばcountの中に2桁以上のdigitがあったなら?
もしこの固定長ルールを破ろうものならElixirはこんな風に文句を言ってきます:
"say:" <> number <> ":" <> thing = "say:123:hi"
# ** (CompileError) a binary field without size is only allowed at the end of a binary pattern
長さの種類が限られているなら、パターンを追加すれば良さげです:
def run_command("say:" <> <<number::bytes-size(1)>> <> ":" <> thing), do: #…
def run_command("say:" <> <<number::bytes-size(2)>> <> ":" <> thing), do: #…
しかしどんな長さの数字にも対応しようとするとこのやり方は使えません。
関数の引数でのパターンマッチングを諦めてひとつの関数の中に判断ロジックを付け足すこともできます:
def run_command("say:" <> stuff) do
case String.split(stuff, ":") do
[number, thing] -> # …
[thing] -> # …
end
end
これはこれで動くんですが、Elixirのやり方としてはダサすぎますよね…。
より良い方法があります。文字列を細切れにしてそれから関数の引数でのパターンマッチングに受け渡せば…?1
def run_command(command) do
do_run_command String.split(command, ":")
end
defp do_run_command(["say", number, thing]) do
count = String.to_integer(number)
String.duplicate(thing, count)
end
defp do_run_command(["say", thing]), do: thing
ああ、だいぶいい感じになってきましたね。では、他の関数への受け渡しも行えるようにしてパターンマッチングの真の実力を解放しましょう。
def run_command(command) do
do_run_command String.split(command, ":")
end
defp do_run_command(["say", number, thing]) do
count = String.to_integer(number)
say(thing, count)
end
defp do_run_command(["say", thing]), do: thing
defp say(thing, count) when count < 100, do: String.duplicate(thing, count)
defp say(thing, _count), do: say(thing, 99) <> " etc"
2
何もlistにする必要はありません。正規表現式も辞書としてマッチさせることができます。例えば:
def run_command(command) do
regex = ~r/say:(?<number>\d+):(?<thing>.+)/
captures = Regex.named_captures(regex, command)
do_run_command(captures)
end
defp do_run_command(%{"number" => number, "thing" => thing}), do: #…
ここまで挙げた例がだんだんわざとらしくなってきてしまいましたが、これで何かアイデアが浮かんでくるといいですね3。