檢視原始碼 Kernel.SpecialForms (Elixir v1.16.2)

特殊形式是 Elixir 的基本建構區塊,因此開發人員無法覆寫。

Kernel.SpecialForms 模組僅包含巨集,可以在 Elixir 程式碼中的任何位置呼叫,而不需要使用 Kernel.SpecialForms. 前綴。這是可能的,因為它們都已自動匯入,與 Kernel 模組中的函式和巨集相同。

這些建構區塊定義在此模組中。其中一些特殊形式是字彙的(例如 alias/2case/2)。巨集 {}/1<<>>/1 也是特殊形式,分別用於定義元組和二進制資料結構。

此模組也記載巨集,這些巨集會傳回有關 Elixir 編譯環境的資訊,例如 (__ENV__/0__MODULE__/0__DIR__/0__STACKTRACE__/0__CALLER__/0)。

此外,它記載了兩個特殊形式,__block__/1__aliases__/1,它們並非供開發人員直接呼叫,但它們會出現在引號內容中,因為它們在 Elixir 的建構中至關重要。

摘要

函式

比對或建立結構體。

%{}

建立映射。

擷取運算子。擷取或建立匿名函式。

點運算子。定義遠端呼叫、呼叫匿名函式或別名。

內部特殊形式,用於儲存別名資訊。

區塊表達式的內部特殊形式。

傳回目前的呼叫環境,為 Macro.Env 結構。

傳回目前檔案目錄的絕對路徑,為二進制。

傳回目前的環境資訊,為 Macro.Env 結構。

傳回目前的模組名稱,為原子或 nil

傳回目前處理的例外堆疊追蹤。

類型運算子。類型和位元串用來指定類型。

定義新的位元串。

比對運算子。比對右方的值與左方的樣式。

alias/2 用於設定別名,通常與模組名稱一起使用。

比對給定的表達式與給定的子句。

評估第一個評估為真值的子句對應的表達式。

定義匿名函式。

理解讓您可以從可列舉或位元串快速建立資料結構。

從其他模組匯入函式和巨集。

取得任何表達式的表示。

檢查目前處理序信箱中是否有與給定子句比對的訊息。

需要一個模組才能使用其巨集。

使用 Kernel.defoverridable/1 覆寫函式時呼叫覆寫的函式。

評估給定的表達式,並處理可能發生的任何錯誤、退出或拋出。

在引號表達式中取消引號給定的表達式。

取消引號給定的清單,展開其引數。

結合比對子句。

釘選運算子。在比對子句中存取已繫結的變數。

建立元組。

函式

比對或建立結構體。

結構是一個標記映射,允許開發人員為金鑰提供預設值,用於多型分派的標記,以及編譯時間斷言。

結構通常使用 Kernel.defstruct/1 巨集來定義

defmodule User do
  defstruct name: "john", age: 27
end

現在可以如下建立結構

%User{}

結構的底層只是一個映射,其中 :__struct__ 金鑰指向 User 模組

%User{} == %{__struct__: User, name: "john", age: 27}

建立結構時可以提供結構欄位

%User{age: 31}
#=> %{__struct__: User, name: "john", age: 31}

或在模式比對中提取值時也可以提供

%User{age: age} = user

結構也有特定的更新操作

%User{user | age: 28}

結構的優點是它們會驗證提供的金鑰是否為定義結構的一部分。以下範例會失敗,因為 User 結構中沒有 :full_name 金鑰

%User{full_name: "john doe"}

上述語法會保證提供的金鑰在編譯時有效,並保證在執行時提供的引數是結構,否則會失敗並出現 BadStructError

雖然結構是映射,但預設情況下結構不會實作任何為映射實作的協定。請查看 Kernel.defprotocol/2,以取得更多關於結構如何與協定一起用於多型分派資訊。另請參閱 Kernel.struct/2Kernel.struct!/2,以取得如何動態建立和更新結構的範例。

模式比對結構名稱

除了允許在結構欄位上進行模式比對,例如

%User{age: age} = user

結構還允許在結構名稱上進行模式比對

%struct_name{} = user
struct_name #=> User

當你想要檢查某個東西是否為結構,但對其名稱不感興趣時,也可以將結構名稱指定給 _

%_{} = user

建立映射。

有關地圖、其語法以及存取和操作它們的方式,請參閱 Map 模組以取得更多資訊。

AST 表示法

不論使用 => 或關鍵字語法,地圖中的鍵值對為了簡潔起見,在內部總是表示為兩元素組成的元組清單

iex> quote do
...>   %{"a" => :b, c: :d}
...> end
{:%{}, [], [{"a", :b}, {:c, :d}]}

擷取運算子。擷取或建立匿名函式。

擷取

擷取運算子最常使用於從模組擷取具有給定名稱和元數的函式

iex> fun = &Kernel.is_atom/1
iex> fun.(:atom)
true
iex> fun.("string")
false

在上述範例中,我們將 Kernel.is_atom/1 擷取為匿名函式,然後呼叫它。

擷取運算子也可以用於擷取區域函式,包括私有函式,以及透過省略模組名稱來匯入函式

&local_function/1

另請參閱 Function.capture/3

匿名函式

擷取運算子也可以用於部分套用函式,其中 &1&2 等等可以用作值佔位符。例如

iex> double = &(&1 * 2)
iex> double.(2)
4

換句話說,&(&1 * 2) 等同於 fn x -> x * 2 end

我們可以使用佔位符部分套用遠端函式

iex> take_five = &Enum.take(&1, 5)
iex> take_five.(1..10)
[1, 2, 3, 4, 5]

使用匯入或區域函式的另一個範例

iex> first_elem = &elem(&1, 0)
iex> first_elem.({0, 1})
0

& 運算子可以用於更複雜的表達式

iex> fun = &(&1 + &2 + &3)
iex> fun.(1, 2, 3)
6

以及清單和組元

iex> fun = &{&1, &2}
iex> fun.(1, 2)
{1, 2}

iex> fun = &[&1 | &2]
iex> fun.(1, [2, 3])
[1, 2, 3]

建立匿名函式時唯一的限制是至少必須有一個佔位符,即它至少必須包含 &1,並且不支援區塊表達式

# No placeholder, fails to compile.
&(:foo)

# Block expression, fails to compile.
&(&1; &2)

點運算子。定義遠端呼叫、呼叫匿名函式或別名。

Elixir 中的點 (.) 可用於遠端呼叫

iex> String.downcase("FOO")
"foo"

在上述範例中,我們使用 . 來呼叫 String 模組中的 downcase,傳遞 "FOO" 作為引數。

點也可以用來呼叫匿名函式

iex> (fn n -> n end).(7)
7

在這種情況下,左側有一個函式。

我們也可以使用點號來建立別名

iex> Hello.World
Hello.World

這次,我們合併了兩個別名,定義了最終別名 Hello.World

語法

. 的右側可以是一個以大寫字母開頭的字詞,代表一個別名,一個以小寫字母或底線開頭的字詞,任何有效的語言運算子或任何用單引號或雙引號包住的名稱。這些都是有效的範例

iex> Kernel.Sample
Kernel.Sample

iex> Kernel.length([1, 2, 3])
3

iex> Kernel.+(1, 2)
3

iex> Kernel."+"(1, 2)
3

用單引號或雙引號包住函式名稱總是會進行遠端呼叫。因此 Kernel."Foo" 會嘗試呼叫函式「Foo」,而不是傳回別名 Kernel.Foo。這是設計使然,因為模組名稱比函式名稱更嚴格。

當點號用來呼叫匿名函式時,只有一個運算元,但它仍然使用後綴表示法來撰寫

iex> negate = fn n -> -n end
iex> negate.(7)
-7

引號表示式

當使用 . 時,引號表示式可以採取兩種不同的形式。當右側以小寫字母(或底線)開頭時

iex> quote do
...>   String.downcase("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}

請注意,我們有一個內部元組,包含表示點號的原子 :. 作為第一個元素

{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}

這個元組遵循 Elixir 中的一般引號表示式結構,其中名稱作為第一個引數,一些關鍵字清單作為第二個引數,引數清單作為第三個引數。在這種情況下,引數是別名 String 和原子 :downcase。遠端呼叫中的第二個引數總是是一個原子。

在呼叫匿名函式的情況下,具有點號特殊形式的內部元組只有一個引數,反映了運算子是一元運算子的事實

iex> quote do
...>   negate.(0)
...> end
{{:., [], [{:negate, [], __MODULE__}]}, [], [0]}

當右側是一個別名(即以大寫字母開頭)時,我們會得到

iex> quote do
...>   Hello.World
...> end
{:__aliases__, [alias: false], [:Hello, :World]}

我們在 __aliases__/1 特殊形式文件中有關別名的更多詳細資訊。

取消引號

我們也可以在引號表示式中使用取消引號來產生遠端呼叫

iex> x = :downcase
iex> quote do
...>   String.unquote(x)("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}

類似於 Kernel."FUNCTION_NAME"unquote(x) 將總是產生遠端呼叫,與 x 的值無關。若要透過引號表示式產生別名,需要依賴 Module.concat/2

iex> x = Sample
iex> quote do
...>   Module.concat(String, unquote(x))
...> end
{{:., [], [{:__aliases__, [alias: false], [:Module]}, :concat]}, [],
 [{:__aliases__, [alias: false], [:String]}, Sample]}

內部特殊形式,用於儲存別名資訊。

通常會編譯成原子

iex> quote do
...>   Foo.Bar
...> end
{:__aliases__, [alias: false], [:Foo, :Bar]}

Elixir 將 Foo.Bar 表示為 __aliases__,因此呼叫可以透過運算子 :. 明確地識別。例如

iex> quote do
...>   Foo.bar()
...> end
{{:., [], [{:__aliases__, [alias: false], [:Foo]}, :bar]}, [], []}

每當表達式迭代器看到 :. 作為元組鍵時,就可以確定它表示呼叫,而清單中的第二個引數是原子。

另一方面,別名具有一些屬性

  1. 別名的頭部元素可以是任何在編譯時必須擴充為原子的術語。

  2. 別名的尾部元素保證永遠是原子。

  3. 當別名的頭部元素是原子 :Elixir 時,不會發生擴充。

區塊表達式的內部特殊形式。

這是我們在 Elixir 中有表達式區塊時使用的特殊形式。此特殊形式是私有的,不應直接呼叫

iex> quote do
...>   1
...>   2
...>   3
...> end
{:__block__, [], [1, 2, 3]}

傳回目前的呼叫環境,為 Macro.Env 結構。

在環境中,您可以存取檔案名稱、行號、設定別名、函式和其他內容。

傳回目前檔案目錄的絕對路徑,為二進制。

儘管可以將目錄存取為 Path.dirname(__ENV__.file),但此巨集是一個方便的捷徑。

傳回目前的環境資訊,為 Macro.Env 結構。

在環境中,您可以存取目前的檔案名稱、行號、設定別名、目前的函式和其他內容。

傳回目前的模組名稱,為原子或 nil

雖然模組可以在 __ENV__/0 中存取,但此巨集是一個方便的捷徑。

連結至這個巨集

__STACKTRACE__

檢視原始碼 (自 1.7.0 起) (巨集)

傳回目前處理的例外堆疊追蹤。

它僅在 try/1 表達式的 catchrescue 子句中可用。

若要擷取目前程序的堆疊追蹤,請改用 Process.info(self(), :current_stacktrace)

類型運算子。類型和位元串用來指定類型。

此運算子在 Elixir 中用於兩種不同的場合。它用於類型規格中,以指定變數、函數或類型本身的類型

@type number :: integer | float
@spec add(number, number) :: number

它也可以用於位元字串中,以指定特定位元區段的類型

<<int::integer-little, rest::bits>> = bits

請閱讀 類型規格頁面<<>>/1 中的說明文件,以取得有關類型規格和位元字串的更多資訊。

定義新的位元串。

範例

iex> <<1, 2, 3>>
<<1, 2, 3>>

類型

位元字串由許多區段組成,每個區段都有類型。位元字串中使用 9 種類型

  • 整數
  • 浮點數
  • 位元位元字串 的別名)
  • 位元字串
  • 二進位
  • 位元組二進位 的別名)
  • utf8
  • utf16
  • utf32

如果未指定類型,預設為 整數

iex> <<1, 2, 3>>
<<1, 2, 3>>

Elixir 預設也接受區段為擴充為整數的文字字串

iex> <<0, "foo">>
<<0, 102, 111, 111>>

你可以使用 utf8(預設)、utf16utf32 中的一個來控制字串的編碼方式

iex> <<"foo"::utf16>>
<<0, 102, 0, 111, 0, 111>>

這等於寫

iex> <<?f::utf16, ?o::utf16, ?o::utf16>>
<<0, 102, 0, 111, 0, 111>>

在執行階段,二進位檔案需要明確標記為 binary

iex> rest = "oo"
iex> <<102, rest::binary>>
"foo"

否則,在建構二進位檔案時,我們會收到 ArgumentError

rest = "oo"
<<102, rest>>
** (ArgumentError) argument error

選項

可以使用 - 作為分隔符號來提供許多選項。順序是任意的,因此下列所有選項都是等效的

<<102::integer-native, rest::binary>>
<<102::native-integer, rest::binary>>
<<102::unsigned-big-integer, rest::binary>>
<<102::unsigned-big-integer-size(8), rest::binary>>
<<102::unsigned-big-integer-8, rest::binary>>
<<102::8-integer-big-unsigned, rest::binary>>
<<102, rest::binary>>

單位和大小

比對的長度等於 unit(位元數)乘以 size(長度為 unit 的重複區段數)。

類型預設單位
整數1 位元
浮點數1 位元
二進位8 位元

類型的大小稍有不同。整數的預設大小為 8。

浮點數為 64。對於浮點數,size * unit 的結果必須是 16、32 或 64,分別對應於 IEEE 754 的 binary16、binary32 和 binary64。

對於二進位檔案,預設大小是二進位檔案的大小。只有比對中的最後一個二進位檔案可以使用預設大小。所有其他二進位檔案都必須明確指定其大小,即使比對沒有歧義。例如

iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"}

大小可以是變數或任何有效的防護式

iex> name_size = 5
iex> <<name::binary-size(^name_size), " the ", species::binary>> = <<"Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}

大小可以存取二進位檔案本身中定義的前一個變數

iex> <<name_size::size(8), name::binary-size(name_size), " the ", species::binary>> = <<5, "Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}

但是,它無法存取在二進位檔案/位元字串外部的比對中定義的變數

{name_size, <<name::binary-size(name_size), _rest::binary>>} = {5, <<"Frank the Walrus">>}
** (CompileError): undefined variable "name_size" in bitstring segment

未指定非最後一個大小會導致編譯失敗

<<name::binary, " the ", species::binary>> = <<"Frank the Walrus">>
** (CompileError): a binary field without size is only allowed at the end of a binary pattern

捷徑語法

在傳遞整數值時,也可以使用語法捷徑來指定大小和單位

iex> x = 1
iex> <<x::8>> == <<x::size(8)>>
true
iex> <<x::8*4>> == <<x::size(8)-unit(4)>>
true

此語法反映了有效大小是由大小乘以單位所給定的事實。

修改器

有些類型有相關的修改器,用於清除位元組表示法中的歧義。

修改器相關類型
有號整數
無號(預設)整數
小端整數浮點數utf16utf32
大端(預設)整數浮點數utf16utf32
原生整數浮點數utf16utf32

符號

整數可以是 有號無號,預設為 無號

iex> <<int::integer>> = <<-100>>
<<156>>
iex> int
156
iex> <<int::integer-signed>> = <<-100>>
<<156>>
iex> int
-100

有號無號 僅用於比對二進位(見下文),且僅用於整數。

iex> <<-100::signed, _rest::binary>> = <<-100, "foo">>
<<156, 102, 111, 111>>

位元序

Elixir 有三種位元序選項:biglittlenative。預設為 big

iex> <<number::little-integer-size(16)>> = <<0, 1>>
<<0, 1>>
iex> number
256
iex> <<number::big-integer-size(16)>> = <<0, 1>>
<<0, 1>>
iex> number
1

native 由 VM 在啟動時決定,且會依賴主機作業系統。

二進位/位元串比對

二進位比對是 Elixir 中一項強大的功能,可用於從二進位中擷取資訊,以及樣式比對。

二進位比對本身可用於從二進位中擷取資訊

iex> <<"Hello, ", place::binary>> = "Hello, World"
"Hello, World"
iex> place
"World"

或作為函式定義的一部分,用於樣式比對

defmodule ImageType do
  @png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
                   13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
  @jpg_signature <<255::size(8), 216::size(8)>>

  def type(<<@png_signature, _rest::binary>>), do: :png
  def type(<<@jpg_signature, _rest::binary>>), do: :jpg
  def type(_), do: :unknown
end

效能與最佳化

Erlang 編譯器可以在二進位建立與比對時提供許多最佳化。若要查看最佳化輸出,請設定 bin_opt_info 編譯器選項

ERL_COMPILER_OPTIONS=bin_opt_info mix compile

若要進一步瞭解特定最佳化與效能考量,請查看 Erlang 效能指南的「建立與比對二進位」章節

比對運算子。比對右方的值與左方的樣式。

連結至這個巨集

alias(module, opts)

檢視原始碼 (巨集)

alias/2 用於設定別名,通常與模組名稱一起使用。

範例

alias/2 可用於設定任何模組的別名

defmodule Math do
  alias MyKeyword, as: Keyword
end

在上述範例中,我們已設定 MyKeyword 的別名為 Keyword。因此,現在任何對 Keyword 的參照都會自動替換為 MyKeyword

如果要存取原始 Keyword,可以透過存取 Elixir 來達成

Keyword.values #=> uses MyKeyword.values
Elixir.Keyword.values #=> uses Keyword.values

請注意,在未設定 :as 選項的情況下呼叫 alias 會自動根據模組的最後一個部分設定別名。例如

alias Foo.Bar.Baz

等同於

alias Foo.Bar.Baz, as: Baz

我們也可以在同一行中設定多個模組的別名

alias Foo.{Bar, Baz, Biz}

等同於

alias Foo.Bar
alias Foo.Baz
alias Foo.Biz

詞彙範圍

import/2require/2alias/2 稱為指令,且都具有詞彙範圍。這表示您可以在特定函式中設定別名,而不會影響整體範圍。

警告

如果您別名一個模組,但您沒有使用該別名,Elixir 將會發出一個警告,暗示該別名未被使用。

但如果別名是由巨集自動產生的,Elixir 就不會發出任何警告,因為別名並未明確定義。

這兩種警告行為都可以透過明確設定 :warn 選項為 truefalse 來變更。

連結至這個巨集

case(condition, clauses)

檢視原始碼 (巨集)

比對給定的表達式與給定的子句。

範例

case File.read(file) do
  {:ok, contents} when is_binary(contents) ->
    String.split(contents, "\n")

  {:error, _reason} ->
    Logger.warning "could not find #{file}, assuming empty..."
    []
end

在上面的範例中,我們將 File.read/1 的結果與每個子句「標頭」配對,並執行與第一個配對子句相應的子句「主體」。

如果沒有任何子句配對,就會引發錯誤。因此,可能需要新增一個最終的萬用子句 (例如 _),它將永遠配對。

x = 10

case x do
  0 ->
    "This clause won't match"

  _ ->
    "This clause would match any value (x = #{x})"
end
#=> "This clause would match any value (x = 10)"

變數處理

請注意,在子句中繫結的變數不會外洩到外部內容

case data do
  {:ok, value} -> value
  :error -> nil
end

value
#=> unbound variable value

外部內容中的變數也無法被覆寫

value = 7

case lucky? do
  false -> value = 13
  true -> true
end

value
#=> 7

在上面的範例中,value 將會是 7,而與 lucky? 的值無關。在子句中繫結的變數 value 和在外部內容中繫結的變數 value 是兩個完全不同的變數。

如果您想要針對現有變數進行樣式配對,您需要使用 ^/1 算子

x = 1

case 10 do
  ^x -> "Won't match"
  _ -> "Will match"
end
#=> "Will match"

使用防護器來針對多個值進行配對

雖然無法在單一子句中針對多個樣式進行配對,但可以使用防護器來針對多個值進行配對

case data do
  value when value in [:one, :two] ->
    "#{value} has been matched"

  :three ->
    "three has been matched"
end

評估第一個評估為真值的子句對應的表達式。

cond do
  hd([1, 2, 3]) ->
    "1 is considered as true"
end
#=> "1 is considered as true"

如果所有條件都評估為 nilfalse,就會引發錯誤。因此,可能需要新增一個最終永遠為真值的條件 (任何非 false 和非 nil 的值),它將永遠配對。

範例

cond do
  1 + 1 == 1 ->
    "This will never match"
  2 * 2 != 4 ->
    "Nor this"
  true ->
    "This will"
end
#=> "This will"

定義匿名函式。

有關更多資訊,請參閱 Function

範例

iex> add = fn a, b -> a + b end
iex> add.(1, 2)
3

匿名函式也可以有多個子句。所有子句都應預期有相同數量的引數

iex> negate = fn
...>   true -> false
...>   false -> true
...> end
iex> negate.(false)
true

理解讓您可以從可列舉或位元串快速建立資料結構。

讓我們從一個範例開始

iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

一個理解接受許多產生器和篩選器。 for 使用 <- 算子從其右側的可列舉中萃取值,並將其與左側的樣式進行比對。我們稱它們為產生器

# A list generator:
iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

# A comprehension with two generators
iex> for x <- [1, 2], y <- [2, 3], do: x * y
[2, 3, 4, 6]

也可以提供篩選器

# A comprehension with a generator and a filter
iex> for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n
[2, 4, 6]

篩選器必須評估為真值 (除了 nilfalse 之外的所有內容)。如果篩選器為假值,則會捨棄目前的數值。

產生器也可以用於篩選,因為它會移除任何與 <- 左側樣式不符的數值

iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...>   String.upcase(name)
...> end
["JOHN", "MEG"]

也支援位元串產生器,當您需要組織位元串串流時,它們非常有用

iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]

理解中的變數指派 (無論是在產生器、篩選器或區塊內) 都不會反映在理解之外。

篩選器中的變數指派仍必須傳回真值,否則數值會被捨棄。讓我們看一個範例。假設您有一個關鍵字清單,其中鍵是程式語言,而值是其直接父項。然後,讓我們嘗試計算每個語言的祖父母。您可以嘗試這樣做

iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent = languages[parent], do: {language, grandparent}
[elixir: :prolog]

由於 Erlang 和 Prolog 的祖父母為 nil,因此這些值已被篩選出來。如果您不想要這種行為,一個簡單的選項是將篩選器移到 do-block 內部

iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages do
...>   grandparent = languages[parent]
...>   {language, grandparent}
...> end
[elixir: :prolog, erlang: nil, prolog: nil]

但是,這種選項並不總是可行,因為您可能有進一步的篩選器。另一種方法是將篩選器轉換為產生器,方法是將 = 右側包覆在清單中

iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent <- [languages[parent]], do: {language, grandparent}
[elixir: :prolog, erlang: nil, prolog: nil]

選項 :into:uniq

在上述範例中,推論式傳回的結果總是清單。傳回的結果可透過傳遞選項 :into 來設定,只要它實作 Collectable 通訊協定,它就接受任何結構。

例如,我們可以使用選項 :into 搭配位元串產生器,輕鬆移除字串中的所有空白

iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"

IO 模組提供串流,它們同時是 EnumerableCollectable,以下是使用推論式的大寫回音伺服器

for line <- IO.stream(), into: IO.stream() do
  String.upcase(line)
end

類似地,也可以將 uniq: true 提供給推論式,以保證結果只會在之前未傳回時才加入集合。例如

iex> for x <- [1, 1, 2, 3], uniq: true, do: x * 2
[2, 4, 6]

iex> for <<x <- "abcabc">>, uniq: true, into: "", do: <<x - 32>>
"ABC"

選項 :reduce

選項 :into 允許我們將推論式行為自訂為特定資料類型,例如將所有值放入映射或二進位中,但這並不總夠。

例如,假設您有一個包含字母的二進位,您想要計算每個小寫字母出現的次數,忽略所有大寫字母。例如,對於字串 "AbCabCABc",我們想要傳回映射 %{"a" => 1, "b" => 2, "c" => 1}

如果我們要使用 :into,我們需要一個資料類型來計算它所包含的每個元素的頻率。雖然 Elixir 中沒有這樣的資料類型,但您可以自己實作一個。

一個更簡單的選項是使用推論式來對字母進行對應和篩選,然後我們呼叫 Enum.reduce/3 來建構映射,例如

iex> letters = for <<x <- "AbCabCABc">>, x in ?a..?z, do: <<x>>
iex> Enum.reduce(letters, %{}, fn x, acc -> Map.update(acc, x, 1, & &1 + 1) end)
%{"a" => 1, "b" => 2, "c" => 1}

雖然上述方法很直接,但它的缺點是至少要遍歷資料兩次。如果您預期輸入會是長字串,這可能會相當耗費資源。

幸運的是,推論式也支援選項 :reduce,這讓我們可以將上述兩個步驟融合為一個步驟

iex> for <<x <- "AbCabCABc">>, x in ?a..?z, reduce: %{} do
...>   acc -> Map.update(acc, <<x>>, 1, & &1 + 1)
...> end
%{"a" => 1, "b" => 2, "c" => 1}

當給定 :reduce 鍵時,它的值會用作初始累加器,而 do 區塊必須變更為使用 -> 子句,其中 -> 的左側接收前一次反覆運算的累積值,而右側的運算式必須傳回新的累加器值。一旦沒有更多元素,就會傳回最後的累積值。如果完全沒有元素,則傳回初始累加器值。

連結至這個巨集

import(module, opts)

檢視原始碼 (巨集)

從其他模組匯入函式和巨集。

import/2 允許您輕鬆存取其他模組中的函式或巨集,而不用使用限定名稱。

範例

如果您正在使用來自特定模組的數個函式,您可以匯入那些函式並將它們作為區域函式參照,例如

iex> import List
iex> flatten([1, [2], 3])
[1, 2, 3]

選取器

預設情況下,Elixir 會匯入來自特定模組的函式和巨集,但會排除以底線開頭的函式(這些函式通常是回呼函式)

import List

開發人員可透過 :only 選項,篩選僅匯入函式、巨集或符號(這些符號可以是函式或巨集)

import List, only: :functions
import List, only: :macros
import Kernel, only: :sigils

或者,Elixir 允許開發人員將名稱/元組對傳遞給 :only:except,以精細地控制要匯入(或不匯入)的內容

import List, only: [flatten: 1]
import String, except: [split: 2]

再次匯入同一個模組會清除先前的匯入,但使用 except 選項時除外,該選項在先前宣告的 import/2 中始終是獨佔的。如果沒有先前的匯入,則它會套用至模組中的所有函式和巨集。例如

import List, only: [flatten: 1, keyfind: 4]
import List, except: [flatten: 1]

在上述兩個匯入呼叫之後,只會匯入 List.keyfind/4

底線函式

預設情況下,不匯入以 _ 開頭的函式。如果您真的想要匯入以 _ 開頭的函式,您必須明確將它包含在 :only 選取器中。

import File.Stream, only: [__build__: 3]

詞彙範圍

請務必注意 import/2 是詞彙的。這表示您可以在特定函式中匯入特定巨集

defmodule Math do
  def some_function do
    # 1) Disable "if/2" from Kernel
    import Kernel, except: [if: 2]

    # 2) Require the new "if/2" macro from MyMacros
    import MyMacros

    # 3) Use the new macro
    if do_something, it_works
  end
end

在上面的範例中,我們從 MyMacros 匯入了巨集,並在那個特定函式中用我們自己的巨集取代了原始 if/2 實作。該模組中的所有其他函式仍可以使用原始巨集。

警告

如果您匯入一個模組,但沒有使用該模組中任何已匯入的函式或巨集,Elixir 會發出警告,表示匯入未被使用。

不過,如果匯入是由巨集自動產生的,Elixir 就不會發出任何警告,因為匯入並未明確定義。

這兩種警告行為都可以透過明確設定 :warn 選項為 truefalse 來變更。

函式/巨集名稱不明確

如果兩個模組 AB 已導入,且它們都包含一個 arity 為 1foo 函數,只有在實際執行對 foo/1 的不明確呼叫時才會發出錯誤;也就是說,這些錯誤是延遲發出,而不是立即發出。

連結至這個巨集

quote(opts, block)

檢視原始碼 (巨集)

取得任何表達式的表示。

範例

iex> quote do
...>   sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}

Elixir 的 AST (抽象語法樹)

任何 Elixir 程式碼都可以使用 Elixir 資料結構表示。Elixir 巨集的建構區塊是一個具有三個元素的元組,例如

{:sum, [], [1, 2, 3]}

上面的元組表示一個函數呼叫,傳遞 1、2 和 3 作為引數給 sum。元組元素為

  • 元組的第一個元素永遠是一個原子或另一個具有相同表示的元組。

  • 元組的第二個元素表示 元資料

  • 元組的第三個元素是函數呼叫的引數。第三個引數可以是一個原子,通常是一個變數(或一個區域呼叫)。

除了上面描述的元組之外,Elixir 還有幾個文字也是其 AST 的一部分。這些文字在被引用時會回傳它們自己。它們是

:sum         #=> Atoms
1            #=> Integers
2.0          #=> Floats
[1, 2]       #=> Lists
"strings"    #=> Strings
{key, value} #=> Tuples with two elements

任何其他值,例如一個映射或一個四元素元組,都必須在引入 AST 之前進行跳脫 (Macro.escape/1)。

選項

  • :bind_quoted - 傳遞一個繫結給巨集。每當給定一個繫結時,unquote/1 會自動停用。

  • :context - 設定解析內容。

  • :generated - 將給定的區塊標記為已產生,因此它不會發出警告。當巨集產生未使用的子句時,這也有助於避免 dialyzer 報告錯誤。

  • :file - 設定引用的表達式具有給定的檔案。

  • :line - 設定引號中的表達式為指定的行。

  • :location - 設定為 :keep 時,會保留引號中的目前行和檔案。請參閱下方的「堆疊追蹤資訊」部分以取得更多資訊。

  • :unquote - 設定為 false 時,會停用取消引號。這表示任何 unquote 呼叫都會保留在 AST 中,而不是由 unquote 參數取代。例如

    iex> quote do
    ...>   unquote("hello")
    ...> end
    "hello"
    
    iex> quote unquote: false do
    ...>   unquote("hello")
    ...> end
    {:unquote, [], ["hello"]}

引號和巨集

quote/2 通常與巨集一起用於產生程式碼。作為練習,我們來定義一個巨集,將數字乘以它本身(平方)。實際上,沒有理由定義這樣的巨集(而且實際上會被視為不良做法),但它夠簡單,讓我們可以專注於引號和巨集的重要面向

defmodule Math do
  defmacro squared(x) do
    quote do
      unquote(x) * unquote(x)
    end
  end
end

我們可以呼叫它為

import Math
IO.puts("Got #{squared(5)}")

一開始,這個範例中沒有任何內容實際上顯示它是一個巨集。但發生的事情是,在編譯時,squared(5) 會變成 5 * 5。參數 5 會在產生的程式碼中重複,我們可以在實際上看到這種行為,因為我們的巨集實際上有一個錯誤

import Math
my_number = fn ->
  IO.puts("Returning 5")
  5
end
IO.puts("Got #{squared(my_number.())}")

上面的範例會印出

Returning 5
Returning 5
Got 25

請注意「Returning 5」印出兩次,而不是一次。這是因為巨集接收表達式,而不是值(這是我們在一般函式中預期的)。這表示

squared(my_number.())

實際上會擴充為

my_number.() * my_number.()

它呼叫函式兩次,說明了為什麼我們會取得印出的值兩次!在大部分情況下,這實際上是預期外的行為,這就是為什麼在使用巨集時,你首先需要記住的事情之一是不要取消引號相同的值超過一次

讓我們修正我們的巨集

defmodule Math do
  defmacro squared(x) do
    quote do
      x = unquote(x)
      x * x
    end
  end
end

現在,呼叫 squared(my_number.()) 如前所述,只會印出值一次。

事實上,這個模式非常常見,在大部分時間,你會想要將 bind_quoted 選項與 quote/2 一起使用

defmodule Math do
  defmacro squared(x) do
    quote bind_quoted: [x: x] do
      x * x
    end
  end
end

:bind_quoted 會轉譯成與上述範例相同的程式碼。 :bind_quoted 可用於許多情況,且被視為良好的實務,不僅因為它有助於防止我們陷入常見錯誤,還因為它允許我們利用巨集公開的其他工具,例如以下部分中討論的取消引號片段。

在我們完成這個簡短的介紹之前,您會注意到,即使我們在引號內定義了一個變數 x

quote do
  x = unquote(x)
  x * x
end

當我們呼叫

import Math
squared(5)
x
** (CompileError) undefined variable "x"

我們可以看到 x 沒有外洩到使用者內容。這是因為 Elixir 巨集是衛生的,我們也會在下一部分中詳細討論這個主題。

變數衛生

考慮以下範例

defmodule Hygiene do
  defmacro no_interference do
    quote do
      a = 1
    end
  end
end

require Hygiene

a = 10
Hygiene.no_interference()
a
#=> 10

在上述範例中, a 會傳回 10,即使巨集顯然將其設定為 1,因為在巨集中定義的變數不會影響巨集執行的內容。如果您想要在呼叫者的內容中設定或取得變數,您可以使用 var! 巨集來執行此操作

defmodule NoHygiene do
  defmacro interference do
    quote do
      var!(a) = 1
    end
  end
end

require NoHygiene

a = 10
NoHygiene.interference()
a
#=> 1

您甚至無法存取在同一個模組中定義的變數,除非您明確地給它一個內容

defmodule Hygiene do
  defmacro write do
    quote do
      a = 1
    end
  end

  defmacro read do
    quote do
      a
    end
  end
end

require Hygiene
Hygiene.write()
Hygiene.read()
** (CompileError) undefined variable "a" (context Hygiene)

為此,您可以明確地傳遞目前的模組範圍作為引數

defmodule ContextHygiene do
  defmacro write do
    quote do
      var!(a, ContextHygiene) = 1
    end
  end

  defmacro read do
    quote do
      var!(a, ContextHygiene)
    end
  end
end

require Hygiene
ContextHygiene.write()
ContextHygiene.read()
#=> 1

變數的內容由元組的第三個元素識別。預設內容為 nil,而 quote 會將另一個內容指定給其中的所有變數

quote(do: var)
#=> {:var, [], Elixir}

對於巨集傳回的變數,元資料中也可能有一個 :counter 鍵,用於進一步精煉其內容,並保證巨集呼叫之間的隔離,如前一個範例所示。

別名衛生

預設情況下,引號內的別名是衛生的。考慮以下範例

defmodule Hygiene do
  alias Map, as: M

  defmacro no_interference do
    quote do
      M.new()
    end
  end
end

require Hygiene
Hygiene.no_interference()
#=> %{}

請注意,即使別名 M 在巨集擴充的內容中不可用,但上述程式碼仍然有效,因為 M 仍會擴充為 Map

同樣地,即使我們在呼叫巨集之前定義了一個具有相同名稱的別名,它也不會影響巨集的結果

defmodule Hygiene do
  alias Map, as: M

  defmacro no_interference do
    quote do
      M.new()
    end
  end
end

require Hygiene
alias SomethingElse, as: M
Hygiene.no_interference()
#=> %{}

在某些情況下,您想要存取呼叫者中定義的別名或模組。為此,您可以使用 alias! 巨集

defmodule Hygiene do
  # This will expand to Elixir.Nested.hello()
  defmacro no_interference do
    quote do
      Nested.hello()
    end
  end

  # This will expand to Nested.hello() for
  # whatever is Nested in the caller
  defmacro interference do
    quote do
      alias!(Nested).hello()
    end
  end
end

defmodule Parent do
  defmodule Nested do
    def hello, do: "world"
  end

  require Hygiene
  Hygiene.no_interference()
  ** (UndefinedFunctionError) ...

  Hygiene.interference()
  #=> "world"
end

匯入衛生

與別名類似,Elixir 中的匯入是衛生的。考慮以下程式碼

defmodule Hygiene do
  defmacrop get_length do
    quote do
      length([1, 2, 3])
    end
  end

  def return_length do
    import Kernel, except: [length: 1]
    get_length
  end
end

Hygiene.return_length()
#=> 3

請注意 Hygiene.return_length/0 回傳 3,即使 Kernel.length/1 函數未匯入。事實上,即使 return_length/0 從另一個模組匯入同名且同參數數量的函數,也不會影響函數結果

def return_length do
  import String, only: [length: 1]
  get_length
end

呼叫這個新的 return_length/0 仍會回傳 3 作為結果。

Elixir 夠聰明,可以將解析延遲到最後一刻。因此,如果你在引號中呼叫 length([1, 2, 3]),但沒有 length/1 函數可用,它會在呼叫者中展開

defmodule Lazy do
  defmacrop get_length do
    import Kernel, except: [length: 1]

    quote do
      length("hello")
    end
  end

  def return_length do
    import Kernel, except: [length: 1]
    import String, only: [length: 1]
    get_length
  end
end

Lazy.return_length()
#=> 5

堆疊追蹤資訊

在透過巨集定義函數時,開發人員可以選擇在呼叫者或引號內報告執行時期錯誤。讓我們看一個範例

# adder.ex
defmodule Adder do
  @doc "Defines a function that adds two numbers"
  defmacro defadd do
    quote location: :keep do
      def add(a, b), do: a + b
    end
  end
end

# sample.ex
defmodule Sample do
  import Adder
  defadd
end

require Sample
Sample.add(:one, :two)
** (ArithmeticError) bad argument in arithmetic expression
    adder.ex:5: Sample.add/2

在使用 location: :keep 且無效參數傳遞給 Sample.add/2 時,堆疊追蹤資訊將指向引號內的檔案和行。沒有 location: :keep 時,錯誤會報告到呼叫 defadd 的地方。 location: :keep 僅影響引號內的定義。

location: :keep 和取消引號

如果函數定義也 unquote 了部分巨集參數,請勿使用 location: :keep。如果你這麼做,Elixir 將儲存目前位置的檔案定義,但取消引號的參數可能包含巨集呼叫者的行資訊,導致堆疊追蹤錯誤。

繫結和取消引號片段

Elixir 引號/取消引號機制提供稱為取消引號片段的功能。取消引號片段提供一種輕鬆的方式來動態產生函數。考慮這個範例

kv = [foo: 1, bar: 2]
Enum.each(kv, fn {k, v} ->
  def unquote(k)(), do: unquote(v)
end)

在上面的範例中,我們動態產生了函數 foo/0bar/0。現在,想像我們想要將這個功能轉換成巨集

defmacro defkv(kv) do
  Enum.map(kv, fn {k, v} ->
    quote do
      def unquote(k)(), do: unquote(v)
    end
  end)
end

我們可以呼叫這個巨集為

defkv [foo: 1, bar: 2]

但是,我們不能這樣呼叫它

kv = [foo: 1, bar: 2]
defkv kv

這是因為巨集預期其參數在編譯時間為關鍵字清單。由於在上面的範例中,我們傳遞的是變數 kv 的表示,我們的程式碼會失敗。

這實際上是開發巨集時常見的陷阱。我們假設巨集中有特定的形狀。我們可以透過在引號表達式中取消引號變數來解決這個問題

defmacro defkv(kv) do
  quote do
    Enum.each(unquote(kv), fn {k, v} ->
      def unquote(k)(), do: unquote(v)
    end)
  end
end

如果你嘗試執行新的巨集,你會注意到它甚至無法編譯,抱怨變數 kv 不存在。這是因為有歧義: unquote(k) 可以是取消引號片段,如前所述,或像 unquote(kv) 中的常規取消引號。

解決此問題的方法之一是停用巨集中的取消引號,但是,這樣做將無法將 kv 表示注入到樹中。這時 :bind_quoted 選項再次派上用場了!透過使用 :bind_quoted,我們可以在注入所需的變數到樹中的同時自動停用取消引號

defmacro defkv(kv) do
  quote bind_quoted: [kv: kv] do
    Enum.each(kv, fn {k, v} ->
      def unquote(k)(), do: unquote(v)
    end)
  end
end

事實上,每次想要將值注入到引號中時,都建議使用 :bind_quoted 選項。

檢查目前處理序信箱中是否有與給定子句比對的訊息。

如果沒有這樣的訊息,目前的程序會暫停,直到訊息到達或等到給定的逾時值。

範例

receive do
  {:selector, number, name} when is_integer(number) ->
    name
  name when is_atom(name) ->
    name
  _ ->
    IO.puts(:stderr, "Unexpected message received")
end

如果在給定的逾時期間後沒有收到訊息,可以提供一個選用的 after 子句,以毫秒為單位指定。

receive do
  {:selector, number, name} when is_integer(number) ->
    name
  name when is_atom(name) ->
    name
  _ ->
    IO.puts(:stderr, "Unexpected message received")
after
  5000 ->
    IO.puts(:stderr, "No message in 5 seconds")
end

即使沒有比對子句,也可以指定 after 子句。提供給 after 的逾時值可以是任何評估為允許值之一的運算式

  • :infinity - 程序應無限期等待比對的訊息,這與不使用 after 子句相同

  • 0 - 如果信箱中沒有比對的訊息,逾時將會立即發生

  • 小於或等於 4_294_967_295 (0xFFFFFFFF 以十六進位表示) 的正整數 - 逾時值應該可以用做一個未簽署的 32 位元整數來表示。

變數處理

receive/1 特殊形式處理變數的方式與 case/2 特殊巨集完全相同。更多資訊,請查看 case/2 的文件。

連結至這個巨集

require(module, opts)

檢視原始碼 (巨集)

需要一個模組才能使用其巨集。

範例

模組中的公用函式是全域可用的,但為了使用巨集,您需要選擇加入,方法是需要它們定義的模組。

假設您在 MyMacros 模組中建立了自己的 if/2 實作。如果您想要呼叫它,您需要先明確需要 MyMacros

defmodule Math do
  require MyMacros
  MyMacros.if do_something, it_works
end

嘗試呼叫未載入的巨集會引發錯誤。

別名捷徑

require/2 也接受 :as 作為選項,因此它會自動設定別名。請查看 alias/2 以取得更多資訊。

使用 Kernel.defoverridable/1 覆寫函式時呼叫覆寫的函式。

請參閱 Kernel.defoverridable/1 以取得更多資訊和文件。

評估給定的表達式,並處理可能發生的任何錯誤、退出或拋出。

範例

try do
  do_something_that_may_fail(some_arg)
rescue
  ArgumentError ->
    IO.puts("Invalid argument given")
catch
  value ->
    IO.puts("Caught #{inspect(value)}")
else
  value ->
    IO.puts("Success! The result was #{inspect(value)}")
after
  IO.puts("This is printed regardless if it failed or succeeded")
end

rescue 子句用於處理例外狀況,而 catch 子句可用於捕捉拋出的值和退出。 else 子句可用於根據表達式的結果控制流程。 catchrescueelse 子句根據模式比對運作(類似於 case 特殊形式)。

try/1 內部的呼叫不是尾遞迴,因為如果發生例外狀況,VM 需要保留堆疊追蹤。若要擷取堆疊追蹤,請在 rescuecatch 子句內存取 __STACKTRACE__/0

rescue 子句

除了依賴模式比對之外,rescue 子句還提供了一些與例外狀況相關的便利功能,允許使用者根據例外狀況的名稱來救援例外狀況。所有下列格式都是 rescue 子句中的有效模式

# Rescue a single exception without binding the exception
# to a variable
try do
  UndefinedModule.undefined_function
rescue
  UndefinedFunctionError -> nil
end

# Rescue any of the given exception without binding
try do
  UndefinedModule.undefined_function
rescue
  [UndefinedFunctionError, ArgumentError] -> nil
end

# Rescue and bind the exception to the variable "x"
try do
  UndefinedModule.undefined_function
rescue
  x in [UndefinedFunctionError] -> nil
end

# Rescue all kinds of exceptions and bind the rescued exception
# to the variable "x"
try do
  UndefinedModule.undefined_function
rescue
  x -> nil
end

Erlang 錯誤

救援時,Erlang 錯誤會轉換為 Elixir 錯誤

try do
  :erlang.error(:badarg)
rescue
  ArgumentError -> :ok
end
#=> :ok

最常見的 Erlang 錯誤會轉換為其對應的 Elixir 錯誤。未轉換的錯誤會轉換為更通用的 ErlangError

try do
  :erlang.error(:unknown)
rescue
  ErlangError -> :ok
end
#=> :ok

事實上,ErlangError 可用於救援任何不是適當 Elixir 錯誤的錯誤。例如,它可用於救援先前轉換前的 :badarg 錯誤

try do
  :erlang.error(:badarg)
rescue
  ErlangError -> :ok
end
#=> :ok

catch 子句

catch 子句可用於捕捉拋出的值、退出和錯誤。

捕捉拋出的值

catch 可用於捕捉 Kernel.throw/1 拋出的值

try do
  throw(:some_value)
catch
  thrown_value ->
    IO.puts("A value was thrown: #{inspect(thrown_value)}")
end

捕捉任何類型的值

catch 子句也支援捕捉退出和錯誤。為此,它允許對捕捉值的類型和值本身進行配對

try do
  exit(:shutdown)
catch
  :exit, value ->
    IO.puts("Exited with value #{inspect(value)}")
end

try do
  exit(:shutdown)
catch
  kind, value when kind in [:exit, :throw] ->
    IO.puts("Caught exit or throw with value #{inspect(value)}")
end

catch 子句也支援 :error,就像在 Erlang 中的 :exit:throw 一樣,儘管這通常會被避免,而改用 raise/rescue 控制機制。這樣做的一個原因是,在捕捉 :error 時,錯誤不會自動轉換為 Elixir 錯誤

try do
  :erlang.error(:badarg)
catch
  :error, :badarg -> :ok
end
#=> :ok

after 子句

after 子句允許您定義清理邏輯,該邏輯將在傳遞給 try/1 的程式碼區塊成功時和發生錯誤時被呼叫。請注意,當收到導致它突然退出的退出訊號時,程序將照常退出,因此無法保證執行 after 子句。幸運的是,Elixir 中的大多數資源(例如開啟的文件、ETS 表格、埠、套接字等)都連結到擁有程序或監控擁有程序,如果該程序退出,它們將自動清除自己。

File.write!("tmp/story.txt", "Hello, World")
try do
  do_something_with("tmp/story.txt")
after
  File.rm("tmp/story.txt")
end

儘管 after 子句會在無論是否有錯誤的情況下被呼叫,但它們不會修改回傳值。以下所有範例都回傳 :return_me

try do
  :return_me
after
  IO.puts("I will be printed")
  :not_returned
end

try do
  raise "boom"
rescue
  _ -> :return_me
after
  IO.puts("I will be printed")
  :not_returned
end

else 子句

else 子句允許對傳遞給 try/1 的主體的結果進行樣式配對

x = 2
try do
  1 / x
rescue
  ArithmeticError ->
    :infinity
else
  y when y < 1 and y > -1 ->
    :small
  _ ->
    :large
end

如果沒有 else 子句,而且沒有引發例外,則會回傳表達式的結果

x = 1
^x =
  try do
    1 / x
  rescue
    ArithmeticError ->
      :infinity
  end

然而,當 else 子句存在,但表達式的結果與任何模式都不相符時,將會引發例外。這個例外不會被同一個 try 中的 catchrescue 捕獲

x = 1
try do
  try do
    1 / x
  rescue
    # The TryClauseError cannot be rescued here:
    TryClauseError ->
      :error_a
  else
    0 ->
      :small
  end
rescue
  # The TryClauseError is rescued here:
  TryClauseError ->
    :error_b
end

類似地,else 子句內的例外不會在同一個 try 內被捕獲或救援

try do
  try do
    nil
  catch
    # The exit(1) call below can not be caught here:
    :exit, _ ->
      :exit_a
  else
    _ ->
      exit(1)
  end
catch
  # The exit is caught here:
  :exit, _ ->
    :exit_b
end

這表示 VM 不再需要在 else 子句內保留堆疊追蹤,因此當在 else 子句內使用 try 時,尾遞迴是可能的,最後一個呼叫是尾呼叫。對 rescuecatch 子句來說也是一樣

只有嘗試表達式的結果會傳遞到 else 子句。如果 try 結束在 rescuecatch 子句中,其結果不會傳遞到 else

try do
  throw(:catch_this)
catch
  :throw, :catch_this ->
    :it_was_caught
else
  # :it_was_caught will not fall down to this "else" clause.
  other ->
    {:else, other}
end

變數處理

由於 try 內的表達式可能因為例外而沒有被評估,因此在 try 內建立的任何變數都無法從外部存取。例如

try do
  x = 1
  do_something_that_may_fail(same_arg)
  :ok
catch
  _, _ -> :failed
end

x
#=> unbound variable "x"

在上面的範例中,x 無法被存取,因為它是在 try 子句內定義的。處理這個問題的常見做法是傳回在 try 內定義的變數

x =
  try do
    x = 1
    do_something_that_may_fail(same_arg)
    x
  catch
    _, _ -> :failed
  end

在引號表達式中取消引號給定的表達式。

這個函式預期一個有效的 Elixir AST,也稱為引號表達式,作為引數。如果你想「取消引號」任何值,例如地圖或四元素組,你應該在取消引號之前呼叫 Macro.escape/1

範例

想像你有一個引號表達式,而且你想要將它注入到某個引號內。第一次嘗試會是

value =
  quote do
    13
  end

quote do
  sum(1, value, 3)
end

其中 :sum 函式呼叫的引數不是預期的結果

{:sum, [], [1, {:value, [], Elixir}, 3]}

對於這個,我們使用 unquote

iex> value =
...>   quote do
...>     13
...>   end
iex> quote do
...>   sum(1, unquote(value), 3)
...> end
{:sum, [], [1, 13, 3]}

如果你想取消引號一個不是引號表達式的值,例如地圖,你需要在之前呼叫 Macro.escape/1

iex> value = %{foo: :bar}
iex> quote do
...>   process_map(unquote(Macro.escape(value)))
...> end
{:process_map, [], [{:%{}, [], [foo: :bar]}]}

如果你忘記跳脫它,Elixir 會在編譯程式碼時引發錯誤。

連結至這個巨集

unquote_splicing(expr)

檢視原始碼 (巨集)

取消引號給定的清單,展開其引數。

類似於 unquote/1

範例

iex> values = [2, 3, 4]
iex> quote do
...>   sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]}

結合比對子句。

了解其運作方式的方法之一,就是展示它改善的程式碼模式。假設您有一個欄位 widthheight 為選配的映射,而且您想要計算它的面積,作為 {:ok, area} 或傳回 :error。我們可以實作這個函數為

def area(opts) do
  case Map.fetch(opts, :width) do
    {:ok, width} ->
      case Map.fetch(opts, :height) do
        {:ok, height} -> {:ok, width * height}
        :error -> :error
      end

    :error ->
      :error
  end
end

當呼叫為 area(%{width: 10, height: 15}) 時,它應該傳回 {:ok, 150}。如果任何欄位遺失,它會傳回 :error

雖然以上的程式碼可以運作,但它相當冗長。使用 with,我們可以將它改寫為

def area(opts) do
  with {:ok, width} <- Map.fetch(opts, :width),
       {:ok, height} <- Map.fetch(opts, :height) do
    {:ok, width * height}
  end
end

我們使用 with 搭配 PATTERN <- EXPRESSION 算子,來比對右側的表達式與左側的模式,而不是定義具有子句的巢狀 case。將 <- 視為 = 的兄弟元素,不同的是,當 = 在不匹配的情況下會引發例外時,<- 只會中止 with 鏈,並傳回未匹配的值。

讓我們在 IEx 中嘗試看看

iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> end
{:ok, 150}

如果所有子句都匹配,do 區塊會執行,並傳回它的結果。否則,鏈會中止,並傳回未匹配的值

iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> end
:error

守衛也可以用在模式中

iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...>   {:ok, to_string(role)}
...> end
{:ok, "admin"}

如同 for/1,繫結在 with/1 內部的變數在 with/1 外部無法存取。

沒有 <- 的表達式也可以用在子句中。例如,您可以使用 = 算子執行正規比對

iex> width = nil
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      double_width = width * 2,
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, double_width * height}
...> end
{:ok, 300}
iex> width
nil

子句中任何表達式的行為都與寫在 with 外部時相同。例如,= 會引發 MatchError,而不是傳回未匹配的值

with :foo = :bar, do: :ok
** (MatchError) no match of right hand side value: :bar

與 Elixir 中的任何其他函式或巨集呼叫一樣,也可以在 do-end 區塊前的參數周圍使用明確的括號

iex> opts = %{width: 10, height: 15}
iex> with(
...>   {:ok, width} <- Map.fetch(opts, :width),
...>   {:ok, height} <- Map.fetch(opts, :height)
...> ) do
...>   {:ok, width * height}
...> end
{:ok, 150}

選擇使用括號或不使用括號取決於喜好。

Else 子句

可以提供 else 選項,以修改在配對失敗的情況下從 with 傳回的內容

iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> else
...>   :error ->
...>     {:error, :wrong_data}
...>
...>   _other_error ->
...>     :unexpected_error
...> end
{:error, :wrong_data}

else 區塊的工作方式類似於 case 子句:它可以有多個子句,且將使用第一個配對。在 with 內繫結的變數(例如此範例中的 width)在 else 區塊中不可用。

如果使用了 else 區塊,且沒有配對的子句,則會引發 WithClauseError 例外狀況。

注意!

請記住, with 的潛在缺點之一是,所有失敗子句都會扁平化成單一 else 區塊。例如,請看這段程式碼,它會檢查給定的路徑是否指向 Elixir 檔案,且在建立備份副本之前,它是否存在

with ".ex" <- Path.extname(path),
     true <- File.exists?(path) do
  backup_path = path <> ".backup"
  File.cp!(path, backup_path)
  {:ok, backup_path}
else
  binary when is_binary(binary) ->
    {:error, :invalid_extension}

  false ->
    {:error, :missing_file}
end

請注意,我們必須重建 Path.extname/1File.exists?/1 的結果類型,才能建立錯誤訊息。在這種情況下,最好重構程式碼,以便每個 <- 在發生錯誤時已傳回所需的格式,如下所示

with :ok <- validate_extension(path),
     :ok <- validate_exists(path) do
  backup_path = path <> ".backup"
  File.cp!(path, backup_path)
  {:ok, backup_path}
end

defp validate_extension(path) do
  if Path.extname(path) == ".ex", do: :ok, else: {:error, :invalid_extension}
end

defp validate_exists(path) do
  if File.exists?(path), do: :ok, else: {:error, :missing_file}
end

請注意,一旦我們確保 with 中的每個 <- 傳回正規化的格式,上面的程式碼就會組織得更好且更清楚。

釘選運算子。在比對子句中存取已繫結的變數。

範例

Elixir 允許變數透過靜態單一指定重新繫結

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

然而,在某些情況下,與其重新繫結,不如比對現有值。這可以使用 ^ 特殊形式來完成,俗稱 pin 運算子

iex> x = 1
iex> ^x = List.first([1])
iex> ^x = List.first([2])
** (MatchError) no match of right hand side value: 2

請注意 ^x 永遠是指比對前的 x 值。下列範例會比對

iex> x = 0
iex> {x, ^x} = {1, 0}
iex> x
1

建立元組。

有關元組資料類型和用於處理元組的函數的更多資訊,請參閱 Tuple 模組;Kernel 中也提供一些用於處理元組的函數(例如 Kernel.elem/2Kernel.tuple_size/1)。

AST 表示法

在 Elixir 中,只有兩個元素的元組才被視為文字,並且在引用時會回傳它們自己。因此,所有其他元組在 AST 中都表示為對 :{} 特殊形式的呼叫。

iex> quote do
...>   {1, 2}
...> end
{1, 2}

iex> quote do
...>   {1, 2, 3}
...> end
{:{}, [], [1, 2, 3]}