我帶著你的心(我把它帶在
我的心裡)不曾放下過(任何
我去的地方你也去了,親愛的;而任何我所做的
就如同你也一起做了,親愛的。)-- E. E. Cummings, i carry your heart with me(i carry it in
-- 0430
走出 ->
的建築後,整個天色似乎比之前亮了一些。而有些原本看似靜止在地上的字,其實在微弱的閃爍。我看見不遠的前方有座噴泉,流淌出來的水發散著柔和的光。走近,卻發現以為是水的東西,裡面有許多許多的符號。或者說,那是液體是符號所構成的什麼。我摸了一下,涼涼的,好像沒有什麼危險。
這可以喝嗎?我想。也許該去找個容器裝一些來研究…
當我們說到容器這個字時,你會想到什麼?大部份的 programmer 都會想到陣列、或是串列,以及鍵值對(JavaScript 的 object 或 Elixir 的 Map) 這兩種東西,如果用過 Python 或是 C# 的人,可能會想到元組 (Tuple)。
在 Haskell 中,串列裡面只能放同一種型別的東西。而它的型別定義,長成這個樣子:
-- Haskell 語法
data [] a = [] | a : [a]
雖然看起來有點奇怪,但是這個定義非常有趣,值得仔細研究一下。這裡說的是裡面裝著 a
這個型別的串列,有下面兩種可能性 ( 記得那個 |
嗎?):要嘛是空串列 []
,要嘛是頭部的一個 a
元素,後面接著…a
型別的串列。
而這個型別定義,就是一種遞迴的定義。串列就是一個頭部的元素,加上一個尾部的串列。我們可以把它用這個定義展開來看:
-- Haskell 語法
list = [1, 2, 3, 4, 5] -- 其實這是一種語法糖。真正的樣子,是下面這樣:
list' = 1 : (2 : (3: (4: (5: []))))
這個定義,保證了串列的最尾巴必然是一個空串列,而裡面的每一個元素,都是同型別的。如果不指定型別,在未放入元素時,我們還不知道那個 a
是什麼。但是一旦放第一個元素進去,那個 a
型別就會被確定下來了。
另一個容器元組 (Tuple),則有更為有趣的性質。元組有非常多種,若是兩個元素的,稱為二元組 (two tuple),三個元素的,稱為三元組 (three tuple 或 triple),而最多可以到 64 個元素。但平常較少用到四個元素以上的。
二元組與三元組的型別定義如下:
-- Haskell 語法
data (,) a b = (,) a b
data (,,) a b c = (,,) a b c -- 三元組
元組有趣的地方,在於它表達了另一種型別的本質,我們稱這一類的型別叫 Product Type。跟上次提到的 Sum Type、也就是|
所代表的加概念相對,它代表的是乘的概念。
所謂乘的概念,就是當你在元組的 a
跟 b
分別指定具體的型別之後,那麼這個型別的可能性的數量,就是 a
型別的可能性乘上 b
性別的可能性。
如果我們讓 a
跟 b
都是布林值的話,那麼我們可以自行將一個 (Bool, Bool)
的型別取個有趣的名稱,例如 PuahPue
(擲筊)。那麼這個型別的可能性總數,就是前面那個布林值的兩種乘上後面那個布林值的兩種,總共四種。而要幫型別取一個別名,用的是 type
關鍵字。
-- Haskell 語法
type PuahPue = (Bool, Bool)
siann_pue = (True, False)
siau_pue = (True, True)
又或者我們可以這樣來表示一張撲克牌的型別:
-- Haskell 語法
data Pip = Two | Three | Four | Five | Six | Seven | Eight | Nine |
Ten | Jack | Queen | King | Ace
data Suit = Spades | Hearts | Diamonds | Clubs
type Card = (Pip, Suit) -- 13 * 4 種可能
card1 = (Ace, Spades)
在實際運用 Haskell,並定義自己應用程式內的型別時,常常會搭配 Sum Type 及 Product Type (也只有這兩種了),來組合出好用的型別定義。
當然,串列跟元組都算是容器。但在 Haskell 裡,對容器這個字的定義更加寬廣。例如我們之前提到的攝氏及華氏溫度,我們可以把它當做一個裝在 Celsius
或是 Fahrenheit
容器裡的一個浮點數。這種容器的特色,就是只能裝一個浮點數。
而在 Haskell 中,有內建了幾個像是這樣的容器,有非常有趣的性質。
在 Haskell 中,是沒有 null
這種概念的*。如果聲明一個變數的型別是 Integer
,那麼就表示這一定是個數字。而有的時候,我們會需要表達這個變數大多數的情況會是個數字,但是有可能發生問題而沒有值時,我們就會用 Maybe Integer
來表示這種情況。
Maybe
的型別定義是這樣:
--- Haskell 語法
data Maybe a = Nothing | Just a
要怎麼使用 Maybe 呢,例如我們要表達一個變數可能是個整數,對於真的是整數的情況,我們會把整數包在 Just
裡。而沒有數字的情況,則用 Nothing
表示:
num1 = Just 1
var2 = Nothing
num3 = Nothing :: Maybe Integer
在上面的範例中,我們可以像第二行那樣,直接宣告 var2 是 Nothing
,接著讓編譯器在後續的型別推導中找到他的型別是什麼。也可以像第三行那樣,直接標明這是一個 Maybe Integer
。這樣如果後續的操作要求裡面的元素是別的型別時,就可以提前告知這個計算有問題了。
註 *:上一章說的那個 undefined
是別的東西,而一般來說並不會把它當成其它語言的 null
來使用。
另一個常用的容器,叫做 Either
。你可以把它看做是 Maybe
的進階版。它的型別定義如下:
-- Haskell 語法
data Either a b = Left a | Right b
在使用 Either 的時候,會把需要接著處理的值 包在 Right
右值裡。而把這是例外情況,不需要進行任何動作的訊息或是其它類的值,放在左值 Left
裡。像是這樣:
-- Haskell 語法
get_user_id = Right 100
get_user_id_failed = Left "Network error"
而除了上面所說的 Maybe
、Eithter
之外,還有一些有趣的內建容器,例如 Sum
、Product
、Any
、All
等等。
當然,還有一個最為有趣,也最難參透的容器。不過這些,就等到我們知道這些容器要怎麼用的時候,再來討論吧。
在其它程式語言裡,想到容器,就覺得是種裡面裝著一堆東西,可以迭代,一個個拿出裡面的元素的東西。而 Haskell 用了更為抽象的視角在看待容器這件事。一旦我們可以指出這些都是容器後,容器們會有一些共通的性質與關係。而理解了這個觀點,可以讓我們用這些共通的性質與關係,推導出許多這些容器們的共通的操作方式。
<故事待補>
*[to be continue]