檢視原始碼 模式和守衛

Elixir 提供模式配對,讓我們可以斷言資料結構的形狀或從中萃取值。模式通常會搭配守衛,讓開發人員可以執行較複雜的檢查,儘管有限。

本文件提供模式和守衛的完整參考,包括其語意、允許使用的地方,以及如何擴充它們。

模式

Elixir 中的模式由變數、文字和資料結構特定語法組成。用於執行模式配對的最常用結構之一是 match 運算子 (=)

iex> x = 1
1
iex> 1 = x
1

在上面的範例中,x 起初沒有值,並將 1 指定給它。然後,我們將 x 的值與文字 1 進行比較,由於它們都是 1,因此成功。

x 與 2 進行配對會引發

iex> 2 = x
** (MatchError) no match of right hand side value: 1

模式不是雙向的。如果你有一個從未指定值的變數 y(通常稱為未繫結變數),而你寫下 1 = y,將會引發錯誤

iex> 1 = y
** (CompileError) iex:2: undefined variable "y"

換句話說,模式只允許出現在 = 的左側。 = 的右側遵循語言的常規評估語意。

現在讓我們介紹每個結構的模式配對規則,然後介紹每個相關資料類型。

變數

模式中的變數總是指定給

iex> x = 1
1
iex> x = 2
2
iex> x
2

換句話說,Elixir 支援重新繫結。如果你不希望變數的值變更,可以使用 pin 運算子 (^)

iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2

如果同一個變數在同一個模式中出現多次,則所有變數都必須繫結到同一個值

iex> {x, x} = {1, 1}
{1, 1}
iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}

底線變數 (_) 具有特殊意義,因為它永遠無法繫結到任何值。當您不關心模式中的特定值時,它特別有用

iex> {_, integer} = {:not_important, 1}
{:not_important, 1}
iex> integer
1
iex> _
** (CompileError) iex:3: invalid use of _

文字 (數字和原子)

原子和數字 (整數和小數) 可以出現在模式中,它們總是按原樣表示。例如,原子只會與原子相符,如果它們是同一個原子

iex> :atom = :atom
:atom
iex> :atom = :another_atom
** (MatchError) no match of right hand side value: :another_atom

類似的規則適用於數字。最後,請注意模式中的數字執行嚴格比較。換句話說,整數與浮點數不相符

iex> 1 = 1.0
** (MatchError) no match of right hand side value: 1.0

元組

元組可以使用大括號語法 ({}) 出現在模式中。模式中的元組只會與大小相同的元組相符,其中每個個別元組元素也必須相符

iex> {:ok, integer} = {:ok, 13}
{:ok, 13}

# won't match due to different size
iex> {:ok, integer} = {:ok, 11, 13}
** (MatchError) no match of right hand side value: {:ok, 11, 13}

# won't match due to mismatch on first element
iex> {:ok, binary} = {:error, :enoent}
** (MatchError) no match of right hand side value: {:error, :enoent}

清單

清單可以使用方括號語法 ([]) 出現在模式中。模式中的清單只會與大小相同的清單相符,其中每個個別清單元素也必須相符

iex> [:ok, integer] = [:ok, 13]
[:ok, 13]

# won't match due to different size
iex> [:ok, integer] = [:ok, 11, 13]
** (MatchError) no match of right hand side value: [:ok, 11, 13]

# won't match due to mismatch on first element
iex> [:ok, binary] = [:error, :enoent]
** (MatchError) no match of right hand side value: [:error, :enoent]

與元組相反,清單還允許使用 [head | tail] 符號對非空清單進行匹配,該符號與清單的 headtail 相符

iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]

多個元素可以作為 | tail 結構的前綴

iex> [first, second | tail] = [1, 2, 3]
[1, 2, 3]
iex> tail
[3]

注意 [head | tail] 不與空清單相符

iex> [head | tail] = []
** (MatchError) no match of right hand side value: []

由於字元清單表示為整數清單,因此也可以使用清單串接運算子 (++) 對字元清單執行前綴匹配

iex> ~c"hello " ++ world = ~c"hello world"
~c"hello world"
iex> world
~c"world"

這等於與 [?h, ?e, ?l, ?l, ?o, ?\s | world] 相符。後綴匹配 (hello ++ ~c" world") 不是有效的模式。

地圖

地圖可能出現在使用百分比符號後接大括號語法的模式中 (%{})。與清單和元組相反,地圖執行子集比對。這表示地圖模式會比對任何包含模式中所有金鑰的其他地圖。

以下是一個所有金鑰都比對的範例

iex> %{name: name} = %{name: "meg"}
%{name: "meg"}
iex> name
"meg"

以下是一個只有子集金鑰比對的範例

iex> %{name: name} = %{name: "meg", age: 23}
%{age: 23, name: "meg"}
iex> name
"meg"

如果模式中的金鑰在地圖中不存在,則它們不會比對

iex> %{name: name, age: age} = %{name: "meg"}
** (MatchError) no match of right hand side value: %{name: "meg"}

請注意,空地圖會比對所有地圖,這與元組和清單形成對比,其中空元組或空清單只會分別比對空元組和空清單

iex> %{} = %{name: "meg"}
%{name: "meg"}

最後,請注意模式中的地圖金鑰必須永遠是文字或先前使用 pin 運算子比對的已繫結變數。

結構

結構可能出現在使用百分比符號、結構模組名稱或變數後接大括號語法的模式中 (%{})。

給定下列結構

defmodule User do
  defstruct [:name]
end

以下是一個所有金鑰都比對的範例

iex> %User{name: name} = %User{name: "meg"}
%User{name: "meg"}
iex> name
"meg"

如果給定未知金鑰,編譯器會產生錯誤

iex> %User{type: type} = %User{name: "meg"}
** (CompileError) iex: unknown key :type for struct User

當放入變數而不是模組名稱時,可以萃取結構名稱

iex> %struct_name{} = %User{name: "meg"}
%User{name: "meg"}
iex> struct_name
User

二進位

二進位可能出現在使用雙小於/大於語法的模式中 (<<>>)。模式中的二進位可以同時比對多個區段,每個區段都有不同的類型、大小和單位

iex> <<val::unit(8)-size(2)-integer>> = <<123, 56>>
"{8"
iex> val
31544

請參閱 <<>> 的文件,以取得二進位模式比對的完整定義。

最後,請記住 Elixir 中的字串是 UTF-8 編碼的二進位。這表示與字元清單類似,字串上的字首比對也可以使用二進位串接運算子 (<>) 執行

iex> "hello " <> world = "hello world"
"hello world"
iex> world
"world"

字尾比對 (hello <> " world") 不是有效的模式。

防護

防護是一種使用更複雜檢查來擴充模式比對的方法。它們允許在允許模式比對的預定義建構中使用,例如函式定義、案例子句等。

並非所有表達式都允許在防護子句中,只有少數幾個允許。這是經過深思熟慮的選擇。這樣,Elixir(和 Erlang)就能確保在執行防護時不會發生任何問題,也不會在任何地方發生變異。它還允許編譯器有效率地最佳化與防護相關的程式碼。

允許的函數和運算子清單

您可以在 Kernel 模組 中找到內建的防護清單。以下是概觀

  • 比較運算子 (==, !=, ===, !==, <, <=, >, >=)
  • 嚴格布林運算子 (and, or, not)。請注意 &&, ||! 兄弟運算子不允許,因為它們不是嚴格布林運算子 - 這表示它們不要求參數為布林值
  • 算術一元運算子 (+, -)
  • 算術二元運算子 (+, -, *, /)
  • innot in 運算子(只要右手邊是清單或範圍)
  • 「類型檢查」函數 (is_list/1, is_number/1 等)
  • 在內建資料類型上運作的函數 (abs/1, hd/1, map_size/1 等)
  • map.field 語法

Bitwise 模組也包含一些 Erlang 位元運算作為防護

由上述任何防護組合建構的巨集也是有效的防護 - 例如,Integer.is_even/1。如需更多資訊,請參閱下方顯示的「自訂模式和防護表達式」區段。

為什麼需要防護

讓我們看一個在函數子句中使用防護的範例

def empty_map?(map) when map_size(map) == 0, do: true
def empty_map?(map) when is_map(map), do: false

防護以 when 運算子開頭,後接防護表達式。當且僅當防護表達式傳回 true 時,才會執行子句。可以使用 andor 運算子來結合多個布林條件。

如果只使用模式比對來撰寫 empty_map?/1 函式是不可能的(因為對 %{} 的模式比對會比對任何一個 map,而不仅仅是空 map)。

不通過的守衛

函式子句將在且僅在它的守衛表達式評估為 true 時執行。如果傳回任何其他值,函式子句將會被略過。特別是,守衛沒有「真值」或「假值」的概念。

例如,想像一個函式檢查一個串列的開頭不是 nil

def not_nil_head?([head | _]) when head, do: true
def not_nil_head?(_), do: false

not_nil_head?(["some_value", "another_value"])
#=> false

即使串列的開頭不是 nilnot_nil_head?/1 的第一個子句會失敗,因為表達式評估的結果不是 true,而是 "some_value",因此觸發傳回 false 的第二個子句。要讓守衛正確運作,你必須確保守衛評估的結果為 true,如下所示

def not_nil_head?([head | _]) when head != nil, do: true
def not_nil_head?(_), do: false

not_nil_head?(["some_value", "another_value"])
#=> true

守衛中的錯誤

在守衛中,當函式通常會引發例外時,它們會導致守衛失敗。

例如,tuple_size/1 函式只適用於 tuple。如果我們將它用於其他任何東西,會引發一個引數錯誤

iex> tuple_size("hello")
** (ArgumentError) argument error

然而,當用於守衛時,對應的子句將無法比對,而不是引發錯誤

iex> case "hello" do
...>   something when tuple_size(something) == 2 ->
...>     :worked
...>   _anything_else ->
...>     :failed
...> end
:failed

在許多情況下,我們可以利用這一點。在上面的程式碼中,我們使用 tuple_size/1 來檢查給定的值是否為 tuple 檢查它的長度(而不是使用 is_tuple(something) and tuple_size(something) == 2)。

然而,如果你的守衛有多個條件,例如檢查 tuple 或 map,最好在 tuple_size/1 之前呼叫型別檢查函式,例如 is_tuple/1,否則如果沒有給定 tuple,整個守衛將會失敗。或者,你的函式子句可以使用多個守衛,如下一節所示。

同一個子句中的多個守衛

還有一種方法可以簡化守衛中 or 表達式的鏈:Elixir 支援在同一個子句中撰寫「多個守衛」。以下程式碼

def is_number_or_nil(term) when is_integer(term) or is_float(term) or is_nil(term),
  do: :maybe_number
def is_number_or_nil(_other),
  do: :something_else

可以另寫為

def is_number_or_nil(term)
    when is_integer(term)
    when is_float(term)
    when is_nil(term) do
  :maybe_number
end

def is_number_or_nil(_other) do
  :something_else
end

如果每個守衛表達式總是傳回布林值,則這兩種形式是等效的。但是,請記住,如果守衛中的任何函數呼叫引發例外,則整個守衛都會失敗。為了說明這一點,以下函數將無法偵測到空元組

defmodule Check do
  # If given a tuple, map_size/1 will raise, and tuple_size/1 will not be evaluated
  def empty?(val) when map_size(val) == 0 or tuple_size(val) == 0, do: true
  def empty?(_val), do: false
end

Check.empty?(%{})
#=> true

Check.empty?({})
#=> false # true was expected!

這可以用來確保不會引發例外,透過類型檢查,例如 is_map(val) and map_size(val) == 0,或使用多個守衛,這樣如果例外導致一個守衛失敗,則會評估下一個守衛。

defmodule Check do
  # If given a tuple, map_size/1 will raise, and the second guard will be evaluated
  def empty?(val)
      when map_size(val) == 0
      when tuple_size(val) == 0,
      do: true

  def empty?(_val), do: false
end

Check.empty?(%{})
#=> true

Check.empty?({})
#=> true

模式和守衛可用於何處

在上面的範例中,我們使用了比對運算子 (=) 和函數子句分別展示模式和守衛。以下是 Elixir 中支援模式和守衛的內建建構清單。

  • match?/2:

    match?({:ok, value} when value > 0, {:ok, 13})
  • 函數子句

    def type(term) when is_integer(term), do: :integer
    def type(term) when is_float(term), do: :float
  • case 表達式

    case x do
      1 -> :one
      2 -> :two
      n when is_integer(n) and n > 2 -> :larger_than_two
    end
  • 匿名函數 (fn/1)

    larger_than_two? = fn
      n when is_integer(n) and n > 2 -> true
      n when is_integer(n) -> false
    end
  • forwith 支援 <- 左側的模式和守衛

    for x when x >= 0 <- [1, -2, 3, -4], do: x

    with 也支援 else 關鍵字,它支援模式比對和守衛。

  • try 支援 catchelse 上的模式和守衛

  • receive 支援模式和守衛來比對接收到的訊息。

  • 自訂守衛也可以使用 defguard/1defguardp/1 定義。自訂守衛只能根據現有守衛定義。

請注意,比對運算子 (=) 支援守衛

{:ok, binary} = File.read("some/file")

自訂模式和守衛表達式

僅此頁面中列出的建構式允許用於模式和防護中。然而,我們可以利用巨集來撰寫自訂模式防護,以簡化我們的程式或使其更具領域特定性。最後,重要的是巨集的輸出會簡化為上述建構式的組合。

例如,Elixir 中的 Record 模組提供一系列巨集,用於模式和防護中,允許在編譯期間為元組命名欄位。

為了定義您自己的防護,Elixir 甚至在 defguarddefguardp 中提供便利性。讓我們來看一個快速案例研究:我們想要檢查一個引數是偶數還是奇數整數。使用模式比對是不可能的,因為整數的數量是無限的,因此我們無法對每個整數進行模式比對。因此,我們必須使用防護。我們只會專注於檢查偶數,因為檢查奇數幾乎相同。

這樣的防護看起來像這樣

def my_function(number) when is_integer(number) and rem(number, 2) == 0 do
  # do stuff
end

每次需要此檢查時重複撰寫會很重複。相反地,您可以使用 defguard/1defguardp/1 來建立防護巨集。以下是如何執行範例

defmodule MyInteger do
  defguard is_even(term) when is_integer(term) and rem(term, 2) == 0
end

然後

import MyInteger, only: [is_even: 1]

def my_function(number) when is_even(number) do
  # do stuff
end

雖然可以使用巨集建立自訂防護,但建議使用 defguard/1defguardp/1 來定義它們,因為它們會執行額外的編譯時間檢查。