檢視原始碼 遞迴

Elixir 不提供迴圈建構式。我們使用遞迴和高階函式來處理集合。本章節將探討前者。

透過遞迴進行迴圈

由於不可變性,Elixir 中的迴圈(與任何函式式程式語言一樣)的寫法與命令式語言不同。例如,在 C 等命令式語言中,會這樣寫

for(i = 0; i < sizeof(array); i++) {
  array[i] = array[i] * 2;
}

在上面的範例中,我們會變異陣列和變數 i。然而,Elixir 中的資料結構是不可變的。因此,函式式語言依賴於遞迴:函式會遞迴呼叫,直到達到停止遞迴動作的條件為止。在此過程中,沒有任何資料會被變異。考慮以下範例,它會印出字串任意次數

defmodule Recursion do
  def print_multiple_times(msg, n) when n > 0 do
    IO.puts(msg)
    print_multiple_times(msg, n - 1)
  end

  def print_multiple_times(_msg, 0) do
    :ok
  end
end

Recursion.print_multiple_times("Hello!", 3)
# Hello!
# Hello!
# Hello!
:ok

case 類似,函式可以有多個子句。當傳遞給函式的引數符合子句的引數模式,且其守衛評估為 true 時,就會執行特定子句。

在上面的範例中,當最初呼叫 print_multiple_times/2 時,引數 n 等於 3

第一個子句有一個守衛,表示「僅當 n 大於 0 時才使用此定義」。由於符合此情況,因此它會印出 msg,然後呼叫自身,並將 n - 1 (2) 傳遞為第二個引數。

現在我們從第一個子句開始再次執行相同函數。假設第二個參數 n 仍然大於 0,我們會印出訊息並再次呼叫自己,現在將第二個參數設定為 1。然後,我們最後一次印出訊息並呼叫 print_multiple_times("Hello!", 0),從頭開始再次執行。

當第二個參數為零時,防護 n > 0 會評估為 false,而第一個函數子句不會執行。然後,Elixir 會嘗試下一個函數子句,它明確比對 n0 的情況。此子句也稱為終止子句,它會透過將訊息參數指定給 _msg 變數來忽略它,並傳回原子 :ok

最後,如果您傳遞不比對任何子句的參數,Elixir 會引發 FunctionClauseError

iex> Recursion.print_multiple_times "Hello!", -1
** (FunctionClauseError) no function clause matching in Recursion.print_multiple_times/2

    The following arguments were given to Recursion.print_multiple_times/2:

        # 1
        "Hello!"

        # 2
        -1

    iex:1: Recursion.print_multiple_times/2

簡化和對應演算法

現在讓我們看看如何利用遞迴的力量來加總數字清單

defmodule Math do
  def sum_list([head | tail], accumulator) do
    sum_list(tail, head + accumulator)
  end

  def sum_list([], accumulator) do
    accumulator
  end
end

IO.puts Math.sum_list([1, 2, 3], 0) #=> 6

我們使用清單 [1, 2, 3] 和初始值 0 作為參數呼叫 sum_list。我們會嘗試每個子句,直到根據比對規則找到一個比對的子句。在此情況下,清單 [1, 2, 3] 比對 [head | tail],它將 head 繫結到 1,將 tail 繫結到 [2, 3]accumulator 設定為 0

然後,我們將清單的開頭加到累加器 head + accumulator,並遞迴地再次呼叫 sum_list,傳遞清單的尾部作為其第一個參數。尾部將再次比對 [head | tail],直到清單為空,如下所示

sum_list [1, 2, 3], 0
sum_list [2, 3], 1
sum_list [3], 3
sum_list [], 6

當清單為空時,它會比對最後一個子句,回傳最後結果 6

把清單取出來並將其簡化為單一值的程序稱為簡化演算法,是函數式程式設計的核心。

如果我們想要將清單中的所有值加倍,該怎麼辦?

defmodule Math do
  def double_each([head | tail]) do
    [head * 2 | double_each(tail)]
  end

  def double_each([]) do
    []
  end
end
$ iex math.exs
iex> Math.double_each([1, 2, 3]) #=> [2, 4, 6]

這裡我們使用遞迴來遍歷清單,將每個元素加倍並回傳新的清單。將清單取出來並對其進行映射的程序稱為映射演算法

遞迴和尾呼叫最佳化是 Elixir 的重要部分,通常用於建立迴圈。然而,當使用 Elixir 程式設計時,你很少會像上面那樣使用遞迴來操作清單。

我們將在下一章節看到的 Enum 模組已經提供了許多便利功能,可用於操作清單。例如,上面的範例可以寫成

iex> Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
6
iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]

或者,使用擷取語法

iex> Enum.reduce([1, 2, 3], 0, &+/2)
6
iex> Enum.map([1, 2, 3], &(&1 * 2))
[2, 4, 6]

讓我們深入探討 Enumerable,以及它的惰性對應項目 Stream