LoginSignup
10
9

More than 5 years have passed since last update.

[翻訳] 複雑な文字列とのパターンマッチング

Posted at

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


  1. 結局ここで文字列のパターンマッチングを諦めてるわけで… 

  2. 100を越える数が指定されたら2番めのsayが呼び出されて99個thingが並んだ後に" etc"を表示しておしまいにします。 

  3. というわけで、文字列を扱うときは他の言語と同じく正規表現を使ってパースするのが一番楽そうですね。やれやれ。もっともその後のところはElixirぽく書けますが。 

10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9