檢視原始碼 遞迴
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 會嘗試下一個函數子句,它明確比對 n
為 0
的情況。此子句也稱為終止子句,它會透過將訊息參數指定給 _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
。