檢視原始碼 模組和函式
在 Elixir 中,我們將多個函式分組到模組中。我們已經在先前的章節中使用過許多不同的模組,例如 String
模組
iex> String.length("hello")
5
為了在 Elixir 中建立我們自己的模組,我們使用 defmodule
巨集。模組的第一個字母必須為大寫。我們使用 def
巨集來定義該模組中的函式。每個函式的第一個字母必須為小寫(或底線)
iex> defmodule Math do
...> def sum(a, b) do
...> a + b
...> end
...> end
iex> Math.sum(1, 2)
3
在本章中,我們將定義我們自己的模組,具有不同的複雜程度。隨著我們的範例長度越來越長,在 shell 中輸入所有範例可能會很棘手。現在是時候學習如何編譯 Elixir 程式碼以及如何執行 Elixir 腳本了。
編譯
大多數時候,將模組寫入檔案中以便編譯和重複使用會比較方便。假設我們有一個名為 math.ex
的檔案,其中包含下列內容
defmodule Math do
def sum(a, b) do
a + b
end
end
可以使用 elixirc
編譯此檔案
$ elixirc math.ex
這將產生一個名為 Elixir.Math.beam
的檔案,其中包含定義模組的位元組碼。如果我們再次啟動 iex
,我們的模組定義將會可用(前提是 iex
在位元組碼檔案所在的目錄中啟動)
iex> Math.sum(1, 2)
3
Elixir 專案通常會整理到三個目錄中
_build
- 包含編譯產物lib
- 包含 Elixir 程式碼(通常為.ex
檔案)test
- 包含測試(通常為.exs
檔案)
在實際專案中,名為 mix
的建置工具將負責為您編譯和設定適當的路徑。為了學習和方便起見,Elixir 還支援腳本模式,此模式較為靈活,不會產生任何編譯產物。
腳本模式
除了 Elixir 檔案副檔名 .ex
,Elixir 也支援 .exs
檔案進行指令碼編寫。Elixir 對這兩種檔案的處理方式完全相同,唯一的差別在於意圖。.ex
檔案用於編譯,而 .exs
檔案則用於指令碼編寫。此慣例由 mix
等專案遵循。
例如,我們可以建立一個名為 math.exs
的檔案
defmodule Math do
def sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2)
並執行如下
$ elixir math.exs
由於我們使用 elixir
而不是 elixirc
,因此模組已編譯並載入至記憶體,但沒有將 .beam
檔案寫入磁碟。在以下範例中,我們建議您將程式碼寫入指令碼檔案,並如上所示執行。
函式定義
在模組內,我們可以使用 def/2
定義函式,並使用 defp/2
定義私有函式。使用 def/2
定義的函式可以從其他模組呼叫,而私有函式只能在本地呼叫。
defmodule Math do
def sum(a, b) do
do_sum(a, b)
end
defp do_sum(a, b) do
a + b
end
end
IO.puts Math.sum(1, 2) #=> 3
IO.puts Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)
函式宣告也支援防護和多個子句。如果函式有多個子句,Elixir 會嘗試每個子句,直到找到一個符合的子句。以下是檢查給定數字是否為零的函式實作
defmodule Math do
def zero?(0) do
true
end
def zero?(x) when is_integer(x) do
false
end
end
IO.puts Math.zero?(0) #=> true
IO.puts Math.zero?(1) #=> false
IO.puts Math.zero?([1, 2, 3]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0) #=> ** (FunctionClauseError)
zero?
中的尾隨問號表示此函式會傳回布林值。若要進一步了解 Elixir 中模組、函式名稱、變數等的命名慣例,請參閱 命名慣例。
提供與任何子句都不相符的引數會引發錯誤。
與 if
等建構類似,函式定義支援 do:
和 do
區塊語法,如 我們在前一章節中所學。例如,我們可以編輯 math.exs
以使其如下所示
defmodule Math do
def zero?(0), do: true
def zero?(x) when is_integer(x), do: false
end
它將提供相同的行為。您可以使用 do:
進行單行編寫,但對於跨多行的函式,請務必使用 do
區塊。如果您偏好一致性,可以在整個程式碼庫中使用 do
區塊。
預設引數
Elixir 中的函式定義也支援預設引數
defmodule Concat do
def join(a, b, sep \\ " ") do
a <> sep <> b
end
end
IO.puts Concat.join("Hello", "world") #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
任何表達式都可以作為預設值,但它不會在函式定義期間評估。每次呼叫函式且必須使用其任何預設值時,都會評估該預設值的表達式
defmodule DefaultTest do
def dowork(x \\ "hello") do
x
end
end
iex> DefaultTest.dowork()
"hello"
iex> DefaultTest.dowork(123)
123
iex> DefaultTest.dowork()
"hello"
如果一個具有預設值的函式有多個子句,則需要建立一個函式標頭(沒有主體的函式定義)來宣告預設值
defmodule Concat do
# A function head declaring defaults
def join(a, b \\ nil, sep \\ " ")
def join(a, b, _sep) when is_nil(b) do
a
end
def join(a, b, sep) do
a <> sep <> b
end
end
IO.puts Concat.join("Hello", "world") #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello") #=> Hello
當一個變數未被函式或子句使用時,我們會在變數名稱前面加上底線 (_
) 來標示此意圖。此規則也在我們的 命名慣例 文件中說明。
在使用預設值時,必須小心避免重疊的函式定義。請考慮以下範例
defmodule Concat do
def join(a, b) do
IO.puts "***First join"
a <> b
end
def join(a, b, sep \\ " ") do
IO.puts "***Second join"
a <> sep <> b
end
end
Elixir 會發出以下警告
warning: this clause cannot match because a previous clause at line 2 always matches
concat.ex:7: Concat
編譯器告訴我們,呼叫 join
函式並傳入兩個參數時,總是會選擇 join
的第一個定義,而第二個定義只會在傳入三個參數時才會被呼叫
$ iex concat.ex
iex> Concat.join "Hello", "world"
***First join
"Helloworld"
iex> Concat.join "Hello", "world", "_"
***Second join
"Hello_world"
在此情況下,移除預設參數將會解決此警告。
這結束了我們對模組的簡短介紹。在接下來的章節中,我們將學習如何使用函式定義進行遞迴,並在之後探索更多與模組相關的功能。