iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 18
0
Software Development

mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉系列 第 18

mostly:functional 第十七章:當我們談論容器時,我們在談論什麼?

我帶著你的心(我把它帶在
我的心裡)不曾放下過(任何
我去的地方你也去了,親愛的;而任何我所做的
就如同你也一起做了,親愛的。)

-- 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、也就是| 所代表的概念相對,它代表的是的概念。

所謂的概念,就是當你在元組的 ab 分別指定具體的型別之後,那麼這個型別的可能性的數量,就是 a 型別的可能性乘上 b 性別的可能性。

如果我們讓 ab 都是布林值的話,那麼我們可以自行將一個 (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 中,有內建了幾個像是這樣的容器,有非常有趣的性質。

Maybe

在 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

另一個常用的容器,叫做 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"

更多內建的容器以及…

而除了上面所說的 MaybeEithter 之外,還有一些有趣的內建容器,例如 SumProductAnyAll 等等。

當然,還有一個最為有趣,也最難參透的容器。不過這些,就等到我們知道這些容器要怎麼用的時候,再來討論吧。


為什麼要說這些東西是容器?

在其它程式語言裡,想到容器,就覺得是種裡面裝著一堆東西,可以迭代,一個個拿出裡面的元素的東西。而 Haskell 用了更為抽象的視角在看待容器這件事。一旦我們可以指出這些都是容器後,容器們會有一些共通的性質與關係。而理解了這個觀點,可以讓我們用這些共通的性質與關係,推導出許多這些容器們的共通的操作方式




<故事待補>

*[to be continue]


上一篇
mostly:functional 第十六章:函數自身
下一篇
mostly:functional 第十八章:不同事物的相同部份
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35

尚未有邦友留言

立即登入留言