之前提過,在 OO 語言裡,認為資料及操作一群相關的資料的行為應該放在同一個地方,形成「物件」這種概念。
而在函數式編程裡,卻用完全不同的角度在看待資料結構以及行為之間的關係。資料本身,就只是資料而己,函數式編程更在乎的,是資料的結構。也就是我們如何用適切的方式,來表達這一群資料彼此之間的關係。而各種集合就是用來表達關係的工具。以下我們來介紹最重要的幾種集合類型、其用途,及一些專用函式。
串列是最基本的工具,它通常用來代表一群類似的東西。在之前的章節裡提到過,elixir 裡的串列寫法是 [1, 2, 3, 4, 5]
。但其實這只是顯示跟撰寫時的語法糖而己。當你寫下上面那個串列,他真正形狀長這個樣子:
[1 | [2 | [3 | [4 | [5 | []]]]]
也就是一個首值,加上尾巴的另一個串列,而最末端是一個空串列。這種資料結構叫做 linked list。由於不是其它語言常見的 Array (陣列),因此他沒有直接取第 n 個值的語法,得要透過函式來遍歷。愈長的串列,計算長度跟讀取後面的值時,所需要的時間就會線性的增長。
之前提過,把 |
用在 pattern matching 上可以把串列切成首值及尾部串列:
[h | t] = [1, 2, 3, 4]
#=> h = 1, t = [2, 3, 4]
想連接兩個串列,變成一個新的串列時,要用 ++/2
。而 --
則可以取差集。
[1, 2, 3] ++ [4, 5, 6]
#=> [1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5] -- [4, 1]
#=> [2, 3, 5]
串列裡的數字,如果都在 ASCII 的守備範圍裡時,印出來會變成 charlist:
[104, 101, 108, 108, 111]
#=> 'hello'
除了剛剛提到的 ++/2
、--/2
以及 length/1
之外,其它 List 的專用函式會放在 List
模組裡。好用的有 List.wrap/1
、List.insert_at/3
、List.pop_at/3
、List.update_at/3
等等。
這種資料類型學過 Python 的人會比較熟悉。Tuple 可以視為固定長度的串列。語法是 {:ok, "Hello"}
。單獨使用時,最常拿來做狀態的 pattern matching。例如絕大多數可能失敗的 Elixir 內建函式操作,都會回傳 {:ok, data}
或是 {:error, message}
。
例如讀檔案:
{:ok, content} = File.read(my_file)
這樣 content
只有在成功讀取檔案時才會被綁定。失敗時這行就會直接噴 MatchError
。如果想要處理失敗的情況,要用到之後會說明的 case
語法:
case File.read(my_file) do
{:ok, content} -> do_something(content)
{:error, msg} -> IO.puts msg
end
如果是個有兩個元素的元組,就稱為 2-tuple,中文翻二元組。同理三個元素的元組叫 3-tuple,三元組。元組通常小於四個,如果太多很可能是你錯用了這個資料結構。
最常用的函式是 elem/2
,讀取特定元素,及 put_elem/3
,在特定位置更新元素及 tuple_size/1
。而其它的專用函式都放在 Tuple
模組裡。
在函數式語言裡,常常會看到二元組的串列這種資料結構,這在 elixir 中稱為 keyword list。前面那個元組是 key,後面那個元組是 value:
[{:a, 1}, {:b, 2}]
Elixir 也為這種資料結構提供了語法糖:
[a: 1, b:2]
使用在函式宣告或是呼叫的最後一個參數時 ,還可以進一步把方框號省略:
# 宣告
def box(height, width, length, opts \\ color: color, top: top) do
do_something
end
box(1, 2, 3, color
Keyword list 除了可以拿來做鍵可重覆的有序鍵值對資料結構外,大多拿來做為函式的可選用參數 (optional parameter)。
除了可以使用串列能用的所有函式外,其它 Keyword list 專用函式都放在 Keyword
模組下。
Map 也是非常典型的集合資料結構,表示鍵不重覆的無序鍵值對。由於 Erlang 在 2014 的 OTP R17 版才引入這個資料型別,而且初推出時,當內容變多時處理速度極慢。因此雖然 elixir 從最一開始就有 map 資料型別,但舊版的教程常常會教你改用比較難操作,但快很多的 HashDict。不過隨著新版 Erlang 提升 Map 的操作速度後,HashDict 也已經被棄用了。
Map 的語法很像 Ruby 的 Hash 語法,只是前方多了一個百分比的 %{}
。連用 atom
當 key 的語法糖都一樣:
%{"a" => 1, "b" => 2}
%{a: 1, b: 2} #=> %{ :a => 1, :b => 2}
Map 的 key 可以是任何值,包含整數、其它資料結構等等。但最常用的還是以字串或 atom 為 key。
與其它程式語言慣例相仿,想要取出特定鍵的值,就會用 []
。但若鍵為 atom 的話,也可以用 .
foo = %{"a" => 1, "b" => 2}
foo["b"] #=> 2
bar = %{c: 3, d: 4}
bar[:c] => 3
bar.c => 3
當你有需要對鍵值對 pattern matching 時,就會用 map 而非 keyword list。另外值得一提的是,用空的 map 放在左手側去與任何 map 進行 pattern matching,都視為成功。
%{a: a, b: b} = %{b: 2, a: 1} #=> a = 1, b = 2
%{} = %{a: 1, b: 2, c: 3} # => 回傳 %{a: 1, b: 2, c: 3}
處理巢狀的 map 時, Kernel
下的 get_in/2
、 put_in/3
、update_in/3
及 get_and_update_in/3
會讓你的程式簡潔許多。而其它的函式照慣例都放在 Map
模組下。
結束了兩天比較瑣碎、條列式的部份,我們總算有足夠的工具往下走了。今天稍早提到了函數式編程在乎的是資料的結構,這只是故事的一半,另一半,就是如何用函式處理這些資料結構。那就請期待明天的主題囉!
Happy hacking! 明天見。