檢視原始碼 Kernel.SpecialForms (Elixir v1.16.2)
特殊形式是 Elixir 的基本建構區塊,因此開發人員無法覆寫。
Kernel.SpecialForms
模組僅包含巨集,可以在 Elixir 程式碼中的任何位置呼叫,而不需要使用 Kernel.SpecialForms.
前綴。這是可能的,因為它們都已自動匯入,與 Kernel
模組中的函式和巨集相同。
這些建構區塊定義在此模組中。其中一些特殊形式是字彙的(例如 alias/2
和 case/2
)。巨集 {}/1
和 <<>>/1
也是特殊形式,分別用於定義元組和二進制資料結構。
此模組也記載巨集,這些巨集會傳回有關 Elixir 編譯環境的資訊,例如 (__ENV__/0
、__MODULE__/0
、__DIR__/0
、__STACKTRACE__/0
和 __CALLER__/0
)。
此外,它記載了兩個特殊形式,__block__/1
和 __aliases__/1
,它們並非供開發人員直接呼叫,但它們會出現在引號內容中,因為它們在 Elixir 的建構中至關重要。
摘要
函式
比對或建立結構體。
建立映射。
擷取運算子。擷取或建立匿名函式。
點運算子。定義遠端呼叫、呼叫匿名函式或別名。
內部特殊形式,用於儲存別名資訊。
區塊表達式的內部特殊形式。
傳回目前的呼叫環境,為 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/2
和 Kernel.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]}, [], []}
每當表達式迭代器看到 :.
作為元組鍵時,就可以確定它表示呼叫,而清單中的第二個引數是原子。
另一方面,別名具有一些屬性
別名的頭部元素可以是任何在編譯時必須擴充為原子的術語。
別名的尾部元素保證永遠是原子。
當別名的頭部元素是原子
:Elixir
時,不會發生擴充。
區塊表達式的內部特殊形式。
這是我們在 Elixir 中有表達式區塊時使用的特殊形式。此特殊形式是私有的,不應直接呼叫
iex> quote do
...> 1
...> 2
...> 3
...> end
{:__block__, [], [1, 2, 3]}
傳回目前的呼叫環境,為 Macro.Env
結構。
在環境中,您可以存取檔案名稱、行號、設定別名、函式和其他內容。
傳回目前檔案目錄的絕對路徑,為二進制。
儘管可以將目錄存取為 Path.dirname(__ENV__.file)
,但此巨集是一個方便的捷徑。
傳回目前的環境資訊,為 Macro.Env
結構。
在環境中,您可以存取目前的檔案名稱、行號、設定別名、目前的函式和其他內容。
傳回目前的模組名稱,為原子或 nil
。
雖然模組可以在 __ENV__/0
中存取,但此巨集是一個方便的捷徑。
傳回目前處理的例外堆疊追蹤。
它僅在 try/1
表達式的 catch
和 rescue
子句中可用。
若要擷取目前程序的堆疊追蹤,請改用 Process.info(self(), :current_stacktrace)
。
類型運算子。類型和位元串用來指定類型。
此運算子在 Elixir 中用於兩種不同的場合。它用於類型規格中,以指定變數、函數或類型本身的類型
@type number :: integer | float
@spec add(number, number) :: number
它也可以用於位元字串中,以指定特定位元區段的類型
<<int::integer-little, rest::bits>> = bits
定義新的位元串。
範例
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
(預設)、utf16
和 utf32
中的一個來控制字串的編碼方式
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
此語法反映了有效大小是由大小乘以單位所給定的事實。
修改器
有些類型有相關的修改器,用於清除位元組表示法中的歧義。
修改器 | 相關類型 |
---|---|
有號 | 整數 |
無號 (預設) | 整數 |
小端 | 整數 、浮點數 、utf16 、utf32 |
大端 (預設) | 整數 、浮點數 、utf16 、utf32 |
原生 | 整數 、浮點數 、utf16 、utf32 |
符號
整數可以是 有號
或 無號
,預設為 無號
。
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 有三種位元序選項:big
、little
和 native
。預設為 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/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/2
、require/2
和 alias/2
稱為指令,且都具有詞彙範圍。這表示您可以在特定函式中設定別名,而不會影響整體範圍。
警告
如果您別名一個模組,但您沒有使用該別名,Elixir 將會發出一個警告,暗示該別名未被使用。
但如果別名是由巨集自動產生的,Elixir 就不會發出任何警告,因為別名並未明確定義。
這兩種警告行為都可以透過明確設定 :warn
選項為 true
或 false
來變更。
比對給定的表達式與給定的子句。
範例
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"
如果所有條件都評估為 nil
或 false
,就會引發錯誤。因此,可能需要新增一個最終永遠為真值的條件 (任何非 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]
篩選器必須評估為真值 (除了 nil
和 false
之外的所有內容)。如果篩選器為假值,則會捨棄目前的數值。
產生器也可以用於篩選,因為它會移除任何與 <-
左側樣式不符的數值
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
模組提供串流,它們同時是 Enumerable
和 Collectable
,以下是使用推論式的大寫回音伺服器
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/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
選項為 true
或 false
來變更。
函式/巨集名稱不明確
如果兩個模組 A
和 B
已導入,且它們都包含一個 arity 為 1
的 foo
函數,只有在實際執行對 foo/1
的不明確呼叫時才會發出錯誤;也就是說,這些錯誤是延遲發出,而不是立即發出。
取得任何表達式的表示。
範例
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/0
和 bar/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
如果你嘗試執行新的巨集,你會注意到它甚至無法編譯,抱怨變數 k
和 v
不存在。這是因為有歧義: 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 位元整數來表示。
變數處理
需要一個模組才能使用其巨集。
範例
模組中的公用函式是全域可用的,但為了使用巨集,您需要選擇加入,方法是需要它們定義的模組。
假設您在 MyMacros
模組中建立了自己的 if/2
實作。如果您想要呼叫它,您需要先明確需要 MyMacros
defmodule Math do
require MyMacros
MyMacros.if do_something, it_works
end
嘗試呼叫未載入的巨集會引發錯誤。
別名捷徑
使用 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
子句可用於根據表達式的結果控制流程。 catch
、rescue
和 else
子句根據模式比對運作(類似於 case
特殊形式)。
try/1
內部的呼叫不是尾遞迴,因為如果發生例外狀況,VM 需要保留堆疊追蹤。若要擷取堆疊追蹤,請在 rescue
或 catch
子句內存取 __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
中的 catch
或 rescue
捕獲
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
時,尾遞迴是可能的,最後一個呼叫是尾呼叫。對 rescue
和 catch
子句來說也是一樣
只有嘗試表達式的結果會傳遞到 else
子句。如果 try
結束在 rescue
或 catch
子句中,其結果不會傳遞到 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/1
。
範例
iex> values = [2, 3, 4]
iex> quote do
...> sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]}
結合比對子句。
了解其運作方式的方法之一,就是展示它改善的程式碼模式。假設您有一個欄位 width
和 height
為選配的映射,而且您想要計算它的面積,作為 {: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/1
和 File.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/2
或 Kernel.tuple_size/1
)。
AST 表示法
在 Elixir 中,只有兩個元素的元組才被視為文字,並且在引用時會回傳它們自己。因此,所有其他元組在 AST 中都表示為對 :{}
特殊形式的呼叫。
iex> quote do
...> {1, 2}
...> end
{1, 2}
iex> quote do
...> {1, 2, 3}
...> end
{:{}, [], [1, 2, 3]}