Do you want to pick up from where you left of?
Take me there

函數

在 Elixir 和許多函數式語言中,函數是一等公民。 本課程將會學習 Elixir 中的函數類型、它們因何與眾不同,以及如何使用它們。

匿名 (Anonymous) 函數

正如函數名稱所暗示的,匿名函數沒有名字。 同時與我們在 Enum 課程中看到的那樣,匿名函數經常被傳遞給其他函數。 為了在 Elixir 中定義一個匿名函數,我們需要 fnend 來做為關鍵字。 在這兩個關鍵字之間,我們可以定義任意數量由 -> 分隔的參數 (parameters) 和函數主體 (bodies)。

現在來看一個基本的例子:

iex> sum = fn (a, b) -> a + b end
iex> sum.(2, 3)
5

& 簡寫符號

在 Elixir 中使用匿名函數是非常普遍的做法,因此有一個使用簡寫符號的書寫法:

iex> sum = &(&1 + &2)
iex> sum.(2, 3)
5

正如你可能猜到的那樣,在簡寫變體中我們的參數可以用 &1&2&3 等等。

模式比對

模式比對不僅限於 Elixir 中的變數,同時可以應用於函數簽章 (signatures),我們將在本節中看到。

Elixir 使用模式比對來檢查所有可能的比對選項,並選擇第一個吻合的選項來執行:

iex> handle_result = fn
...>   {:ok, result} -> IO.puts "Handling result..."
...>   {:ok, _} -> IO.puts "This would be never run as previous will be matched beforehand."
...>   {:error} -> IO.puts "An error has occurred!"
...> end

iex> some_result = 1
1
iex> handle_result.({:ok, some_result})
Handling result...
:ok
iex> handle_result.({:error})
An error has occurred!

命名 (Named) 函數

我們可以用名字來定義函數,以便稍後可以很容易地引用它們。 在模組中,定義命名函數使用 def 做為關鍵字。 我們將在接下來的課程中學習更多關於模組的內容,現在我們將單單專注於命名函數。

在模組中定義的函數可供其他模組使用。 這是 Elixir 中一個特別有用的構建區塊 (building block):

defmodule Greeter do
  def hello(name) do
    "Hello, " <> name
  end
end

iex> Greeter.hello("Sean")
"Hello, Sean"

如果函數主體只有一行,可以用 do: 來進一步縮短:

defmodule Greeter do
  def hello(name), do: "Hello, " <> name
end

借助我們對模式比對的了解,讓我們探索使用命名函數的遞迴 (recursion):

defmodule Length do
  def of([]), do: 0
  def of([_ | tail]), do: 1 + of(tail)
end

iex> Length.of []
0
iex> Length.of [1, 2, 3]
3

函數命名和引數數目 (Arity)

我們前面提到,函數是通過給定名稱和引數數目 (arity) 的組合來命名的。 這意味著你可以做這樣的事情:

defmodule Greeter2 do
  def hello(), do: "Hello, anonymous person!"   # hello/0
  def hello(name), do: "Hello, " <> name        # hello/1
  def hello(name1, name2), do: "Hello, #{name1} and #{name2}"
                                                # hello/2
end

iex> Greeter2.hello()
"Hello, anonymous person!"
iex> Greeter2.hello("Fred")
"Hello, Fred"
iex> Greeter2.hello("Fred", "Jane")
"Hello, Fred and Jane"

我們在上面的註釋中列出了函數名稱。 第一個實現 (implementation) 不接受引數 (arguments),所以它被稱為 hello/0;第二個則接受一個引數,所以它被稱為 hello/1,依此類推。 與某些其他語言中的函數重載 (function overloads) 不同,這些被認為是彼此 不同 的函數。 (前面提過的模式比對只有當函數名字與引數數量都 相同 時才適用)。

函數和模式比對

在背後,函數以模式比對被呼用的參數。

假設需要一個接受映射的函數,但我們只對使用特定鍵感興趣。 那麼可以像這樣用模式比對該參數的鍵:

defmodule Greeter1 do
  def hello(%{name: person_name}) do
    IO.puts "Hello, " <> person_name
  end
end

現在假設有個描述某人名字為 Fred 的映射:

iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }

這是以 fred 映射呼用 Greeter1.hello/1 時得到的結果:

# call with entire map
...> Greeter1.hello(fred)
"Hello, Fred"

當使用 包含 :name 鍵的映射來呼用函數時會發生什麼?

# call without the key we need returns an error
...> Greeter1.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter3.hello/1

    The following arguments were given to Greeter3.hello/1:

        # 1
        %{age: "95", favorite_color: "Taupe"}

    iex:12: Greeter3.hello/1

出現這種行為的原因是 Elixir 會模式比對被呼用函數的參數與定義函數的引數數目。

現在考慮資料到達 Greeter1.hello/1 時的樣子:

# incoming map
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }

Greeter1.hello/1 期待這樣的參數:

%{name: person_name}

Greeter1.hello/1 中,傳入的映射 (fred) 是根據參數 (%{name: person_name}) 賦值:

%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}

它發現在傳入的映射中有一個與 name 對應的鍵。 現在配對成立!並且由於這次成功的配對,右邊映射中 :name 鍵的值(即 fred 映射)將綁定到左邊的變數(person_name)上。

現在,如果仍希望將 Fred 的名字分配給 person_name,但「也」想保留對整個 person 映射的認知呢?假設在問候他之後想要 IO.inspect(fred)。 此時,因為只有對映射中的 :name 鍵進行模式比對,因此只會將該鍵的值綁定到變數,所以函數並不會了解 Fred 的其餘資訊。

為了保留完整資訊,需要將整個映射分配給它自己的變數,以便能夠使用它。

現在建立一個新函數:

defmodule Greeter2 do
  def hello(%{name: person_name} = person) do
    IO.puts "Hello, " <> person_name
    IO.inspect person
  end
end

請記住,參數傳入時 Elixir 會進行模式比對。 因此,在這種情況下,兩邊都將模式比對傳入的參數並綁定到無論它配對到的任何內容。 首先來看右邊:

person = %{name: "Fred", age: "95", favorite_color: "Taupe"}

現在,person 已被賦值並綁定到整個 fred-map。 接著繼續進行下一個模式比對:

%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}

這與原來模式比對映射並且只保留 Fred 名字的 Greeter1 函數相同。 這麼做所取得的成果為可以使用兩個變數而不是只有一個:

  1. person ,指向 %{name: "Fred", age: "95", favorite_color: "Taupe"}
  2. person_name ,指向 "Fred"

所以現在當呼用 Greeter2.hello/1 時,可以使用 Fred 的所有資訊:

# call with entire person
...> Greeter2.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}
# call with only the name key
...> Greeter2.hello(%{name: "Fred"})
"Hello, Fred"
%{name: "Fred"}
# call without the name key
...> Greeter2.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter2.hello/1

    The following arguments were given to Greeter2.hello/1:

        # 1
        %{age: "95", favorite_color: "Taupe"}

    iex:15: Greeter2.hello/1

可以看到 Elixir 在多個深度上模式比對,因為每個參數獨立地配對傳入資料,在函數內部留下變數來呼用它們。

如果在列表中切換 %{name: person_name}person 的順序,將得到相同的結果,因為每個都與 fred 自己配對。

現在交換變數和映射:

defmodule Greeter3 do
  def hello(person = %{name: person_name}) do
    IO.puts "Hello, " <> person_name
    IO.inspect person
  end
end

並以在 Greeter2.hello/1 中使用的相同資料呼用它:

# call with same old Fred
...> Greeter3.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}

請記住,即使 %{name: person_name} = person} 看起來像 %{name: person_name}person 變數進行模式比對,但它們實際上是 各自 模式比對到傳入的參數。

總結: 函數各別地模式比對傳入每個參數的資料。 可以使用它將值綁定到函數中的多個獨立變數。

私有 (Private) 函數

當我們不想讓其他模組存取某個特定的函數時,我們可以讓這個函數被保密。 私有函數只能從自己的模組中呼用。 在 Elixir 中,我們使用 defp 來定義:

defmodule Greeter do
  def hello(name), do: phrase() <> name
  defp phrase, do: "Hello, "
end

iex> Greeter.hello("Sean")
"Hello, Sean"

iex> Greeter.phrase
** (UndefinedFunctionError) function Greeter.phrase/0 is undefined or private
    Greeter.phrase()

監視 (Guards)

我們在 Control Structures 課程中簡要地介紹了監視 (guards),現在我們來看看如何應用到命名函數上。 一旦 Elixir 配對了一個函數,任何現有的監視語句 (guards) 都將被檢驗。

在下面的例子中,我們有兩個具有相同簽章的函數,我們依靠監視 (guards) 來決定使用何種引數類型 (argument’s type) 的函數:

defmodule Greeter do
  def hello(names) when is_list(names) do
    names = Enum.join(names, ", ")
    
    hello(names)
  end

  def hello(name) when is_binary(name) do
    phrase() <> name
  end

  defp phrase, do: "Hello, "
end

iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"

預設 (Default) 引數值

如果我們需要定義引數的預設值,使用 argument \\ value 語法:

defmodule Greeter do
  def hello(name, language_code \\ "en") do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end

iex> Greeter.hello("Sean", "en")
"Hello, Sean"

iex> Greeter.hello("Sean")
"Hello, Sean"

iex> Greeter.hello("Sean", "es")
"Hola, Sean"

當我們把監視的用法和預設的引數值結合起來時,會遇到一個問題。 現在來看看會是什麼樣的情形:

defmodule Greeter do
  def hello(names, language_code \\ "en") when is_list(names) do
    names = Enum.join(names, ", ")
    
    hello(names, language_code)
  end

  def hello(name, language_code \\ "en") when is_binary(name) do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end

** (CompileError) iex:31: definitions with multiple clauses and default values require a header.
Instead of:

    def foo(:first_clause, b \\ :default) do ... end
    def foo(:second_clause, b) do ... end

one should write:

    def foo(a, b \\ :default)
    def foo(:first_clause, b) do ... end
    def foo(:second_clause, b) do ... end

def hello/2 has multiple clauses and defines defaults in one or more clauses
    iex:31: (module)

Elixir 不喜歡多重比對函數中的預設引數值,因可能會造成混淆。 我們加上一個帶有預設引數值的函數標頭 (head) 來處理這個問題:

defmodule Greeter do
  def hello(names, language_code \\ "en")
  def hello(names, language_code) when is_list(names) do
    names = Enum.join(names, ", ")
    
    hello(names, language_code)
  end

  def hello(name, language_code) when is_binary(name) do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end

iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"

iex> Greeter.hello ["Sean", "Steve"], "es"
"Hola, Sean, Steve"
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!