Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

パターンマッチ

3 Pattern match.livemd

パターンマッチ

パターンマッチとは

  • Elixirの強力な構文
  • Elixirにおける=演算子はマッチ演算子と呼ばれ、手続き型の言語の代入演算子とは概念が異なる

マッチ演算子

  • 左辺と右辺をマッチさせる
    • 変数を含まない値同士でマッチ可能
  • 左辺に変数が含まれる場合、右辺とマッチするよう値に「束縛」される
  • 左辺と右辺がマッチしない場合、「MatchError」
# マッチが成功する例
0 = 0
"a" = "a"
:a = :a
[0, 1, 2] = [0, 1, 2]
%{a: 0, b: 1} = %{a: 0, b: 1}
%{a: 0} = %{a: 0, b: 1}

以下, MatchErrorとなる例

0 = 1
[0, 1] = [0, 1, 2]
[0, 1, 2] = [0, 1]
%{a: 0, b: 1} = %{a: 0}

マッチ演算子と変数束縛

  • 変数はマッチ演算子により、右辺にマッチするよう値に束縛される
    • 変数に値を束縛してもマッチできない場合、やはりMatchError
# 一番最初の時点ではxは定義されていないし、何の値にも束縛されていない
x
# => (CompileError) console:2 "undefined function x/0"
# 0とマッチさせると、xに0が束縛されていれば両辺がマッチする。よってxに0が束縛される
x = 0

x
x = 0
# xは0で束縛されているのでマッチが成功
0 = x
x = 0
# => %MatchError{term: 0} xは既に0に束縛されているので、1とはマッチしない
1 = x

Elixirでは値は不変(immutable)

array1 = [0, 1, 2]
array2 = array1
array2 = array2 ++ [3]

# 以下は true か false か? 予想してから実行してみよう
array1 == [0, 1, 2] and array2 == [0, 1, 2, 3]

Listに対するパターンマッチ

# 要素が全て等しいのでマッチが成功
l = [0, 1, 2, 3]
[0, 1, 2, 3] = l
# 変数が含まれる場合、右辺にマッチするよう値が束縛される
[a, b, c, d] = [0, 1, 2, 3]

"#{a}, #{b}, #{c}, #{d}"
# 右辺に変数が含まれる場合、右辺の変数に束縛されている値にマッチする
x = 10
y = 20
[a, b, c, d] = [0, x, 2, y]

"#{a}, #{b}, #{c}, #{d}"

Exercise 3-1: List から必要な値を変数に束縛して取り出してみよう

# xに1が, yに4が束縛されるようパターンマッチを完成させてよう。
= [0, 1, 2, 3, 4, 5]
 
x == 1 and y == 4 # true になること。

不要な値を無視する

Elixirにおいて, アンダースコア_やアンダースコアで始まる変数は、コンパイラに「使用しない」変数であることを表す。

Elixirでは未使用の変数があるとコンパイル時にwarningが発せられる。

アンダースコアを用いて使用しない変数であると明示すれば、warningが解消される。

パターンマッチでもマッチはさせるが使用しない場合に、アンダースコアを用いることができる。

# Listの先頭から3番目の値だけ取り出したいとき。1番目と2番目, 4番目は何でもいい
[_, _, x, _] = [0, 1, 2, 3]

x

Listに対するパターンマッチでは特有のマッチ記法がある。

[a | b]aはリストの先頭、bはその後続のリストにマッチする。

[head | tail] = [0, 1, 2, 3, 4]

head == 0 and tail == [1, 2, 3, 4]

要素が1つだと、tailは空のリストにマッチする。

[head | tail] = [0]

head == 0 and tail == []

空のリストにマッチさせようとすると、MatchError

[head | tail] = []
# => %MatchError{term: []}

先頭から$n$個の要素に対してマッチすることも可能。

反対に、最後から$n$個のマッチは不可能。

[first, second | tail] = [0, 1, 2, 3, 4]

first == 0 and
  second == 1 and
  tail == [2, 3, 4]
# => CompileError
[head | last_one_before, last] = [0, 1, 2]

Listの結合演算子++を用いてパターンマッチさせることも可能。

ただし、変数を用いる場合は++の右辺にしか置けない。

[0] ++ rest = [0, 1, 2, 3]
rest
head ++ [1, 2, 3] = [0, 1, 2, 3]
# => CompileError

Tupleに対するパターンマッチ

要素の数が一致する必要がある

{a, b, c} = {1, "a", :atom}
{a, b} = {1, "a", :atom}

よくあるのは、処理の成功/失敗によって処理を切り替えたい場合。

  • Elixir では成功/失敗をそれぞれ:ok:errorとのタプルを返すことで表現することが多い
result = {:ok, "succeeded"}

{:ok, msg} = result
"Operation #{msg}"

Mapに対するパターンマッチ

  • 左辺は右辺のサブセットであればいい
  • ネストしたマップにもパターンマッチ可能
%{x: value} = %{x: 0, y: 1, z: 2}
value
%{:x => value} = %{x: 0, y: 1, z: 2}
value
%{no_key: value} = %{x: 0, y: 1, z: 2}
# => MatchError
nested_map = %{
  outer_universe: %{
    universe: %{
      hello: "universe!",
      answer_of_everything: 42
    }
  }
}

%{outer_universe: %{universe: %{hello: target}}} = nested_map
target
# マッチ演算子をネストさせることもできる
%{
  outer_universe: %{
    universe:
      %{
        answer_of_everything: the_answer
      } = universe
  }
} = nested_map

IO.inspect(universe)
IO.inspect(the_answer)

文字列に対するパターンマッチ

文字列の結合に<>という演算子を使用できることを2 Basic syntax #特徴的な演算子で紹介したが、この演算子はパターンマッチにも使用できる。

ただし、変数を用いる場合は<>の右辺にしか置けない。

"Hello " <> target = "Hello world!"
target
greet <> " world" = "Hello world!"
# => ArgumentError

pin演算子

  • 変数は通常、別の値にマッチさせると新しい値に束縛される
  • pin演算子を使うと、変数が束縛されている値に対してマッチするか試すことができる
target_1 = %{greet: "Hello", name: "world"}
target_2 = %{greet: "こんにちは", name: "世界"}
# pinned_greet は場合によって動的に変わるとする
# greet: が pinned_greet と等しいときだけパターンマッチが成功するようにしたい
pinned_greet = "Hello"

%{greet: pinned_greet, name: to_be_world} = target_1
pinned_greet == "Hello" and to_be_world == "world"
pinned_greet = "Hello"

try do
  %{greet: pinned_greet, name: to_be_world} = target_2
  pinned_greet == "こんにちは" and to_be_world == "世界"
  # MatchError を期待しているが…果たして?
rescue
  MatchError -> "😲"
end

pinned_greet は変数なので、パターンマッチのたび別の値が束縛されるのだった。

pin 演算子は、変数が新しい値に束縛されないようにする。

pinned_greet = "Hello"

# %{greet: "Hello", name: to_be_world} = target_1 と等価だ
%{greet: ^pinned_greet, name: to_be_world} = target_1
pinned_greet == "Hello" and to_be_world == "world"
pinned_greet = "Hello"

try do
  %{greet: ^pinned_greet, name: to_be_world} = target_2
  to_be_world == "世界"
  # 今度こそ MatchError になるだろう
rescue
  MatchError -> "😃"
end

練習問題

# Exercise 3-2
# 3を変数xに束縛
 = %{a: 1, b: 2, c: 3}

x == 3
# Exercise 3-3
# :aを変数x,2.3を変数yに拘束(1と"a"は何にも拘束しない)
 = [1, :a, "a", 2.3]
 
x == :a and y == 2.3
# Exercise 3-4
# 2を変数x,4を変数y,5~10のリストをzに拘束(1と3は何にも拘束しない)
 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

x == 2 and y == 4 and z == [5, 6, 7, 8, 9, 10]
# Exercise 3-5
# 1を変数x,4を変数yに拘束
 =  %{a: %{b: 1, c: 2, d: [3, 4, 5]}}
 
x == 1 and y == 4
# Exercise 3-6, 3-7で使用するデータ
request = %{
  header: %{
    "x-custom-header": "a8d3981b2"
  },
  body: %{
    first_name: "Alice",
    last_name: "Liddell",
    address: [
      "Westminster",
      "London",
      "England",
      "United Kingdom"
    ]
  }
}
# Exercise 3-6
# 複雑なパターンマッチを試してみよう
# requestのbodyからfirst_nameとlast_nameを同時に取り出してみよう
 = request

first_name == "Alice" and last_name == "Liddell"
# Exercise 3-7
# requestのbodyのaddressは地区, 州, 構成国, 主権国家の順に並んでいる。州(state)と構成国(country)だけ取り出してみよう
 = request

state == "London" &amp;&amp; country == "England"