我們在乎的,不是你的類別,而是你的型別。
用另一種方式講,我們不是那麼在乎你是哪一種人,而在乎你是什麼樣的人。-- 唐鳳, RubyConf 座談, 2014
-- 0610
自從上次從那個建築離開後,我又在這裡碰壁很久了。雖然有些東西看得比較清楚一點,但我發現就算我知道 immutable 跟遞迴什麼的,我在這裡能做的事,還是很少。
而所謂的碰壁,是字面上的意思。這個地方裡,有一大片區域被透明的牆所擋住,而許多看起來不斷在運算的東西,都連到牆的那一邊。而我怎樣都找不到方法或是線索過去。
我只注意到牆上寫了一個符號:>>=
-- 0630
我在周邊的屋子,找到許多其它人寫的手稿。畫有 >>=
那些手稿,都不斷的提到一個字:"Monad"。但是實在看不太懂。有些講了 flat_map
,有些寫什麼「 Monad 只不過是自函子範疇上的么半群」之類的外星文,也有畫了一堆盒子的,還有一篇講什麼墨西哥捲餅的。
-- 0709
就在我決定先離開這座城去其它地方看看時,我在城的入口附近,發現有一塊金屬板浮在空中,上面泛著紫色的光澤。我摸了一下,板子上便開始浮現刻下的字。它寫著:
「From first principle」
「要理解 Monad,第一條建議,便是儘量不要去讀那些總結性的手稿。」好喔。
但正要往下看其它的文字時,金屬板便迅速飛離,還帶著發光的軌跡。這根本就奇幻小說作者找不到哏又想移動場景的手段啊,我想著,但還是跟了上去。軌跡停在一棟建築前面,上面寫著 class
。
class
?又不是物件導向,這東西怎麼會出現在這裡?
在 Haskell 中,class
與物件導向語言裡的類別不是同一種東西。這個 class
代表的是 typeclass、型別類別。這個字說的是,有許多不同的型別,它們都有一些共同的性質,或是行為。例如我們可以判斷兩個數字是否相等,或兩個字串是否相等,但我們不太知道怎麼判斷兩個函式是否相等。又或者一群數字可以互相比較大小,而一群字串間也可以比較大小,一群函式就實在無法彼此比較大小了。
那麼這兩種行為,我們就把它們稱為 typeclass。可以判斷是否相等的,就叫 Eq
,而可以比較大小的 typeclass,就稱為 Ord
,這兩個都是 Haskell 內建的 typeclass。
每個 typeclass 可能有多個相關的性質或行為,例如一旦我們能比較大小,那麼就可以順勢做出在一群這種東西裡,挑出最大的及最小的那個的函式,當然也可以進行排序。
註*:若你有用過 JAVA 或是 C#,你可以暫時把它想成跟 interface (介面) 有點類似的東西,但是有些微的差異。
而要讓一個型別有 typeclass
,有兩種方式,一種是自行實做,另一種是…自動推導。
用我們之前所做的溫度型別為例,我們實作的溫度目前是無法判斷是否相等或是比較大小的:
-- Haskell 語法
data Temp = Celsius Float | Fahrenheit Float
deriving (Show)
temp1 = Celsius 50.0
temp2 = Celsius 50.0
temp3 = Celsius 60.0
temp1 == temp2
-- => <interactive>:3:1: error:
-- • No instance for (Eq Temp) arising from a use of ‘==’
但是我們知道,要判斷兩個攝氏溫度是否相等,只要看裡面的浮點數字是否相等就可以了。而浮點數,本來就已經可以判斷是否相等及比較大小了。那麼我們就可以讓 Haskell 試著去推導,而自動推導的方式,是在型別的定義下方,加上 deriving
。而用來比較相等的 typeclass,叫做 Eq
:
-- Haskell 語法
data Temp = Celsius Float | Fahrenheit Float
deriving (Show, Eq)
temp1 = Celsius 50.0
temp2 = Celsius 50.0
temp1 == tmep2
-- => True
而若要讓兩個溫度能夠比較大小,則要繼續推導 Ord
這個 typeclass:
-- Haskell 語法
data Temp = Celsius Float | Fahrenheit Float
deriving (Show, Eq, Ord)
temp1 = Celsius 50.0
temp3 = Celsius 60.0
temp1 > tmep3
-- => False
要注意的是,若想要推導 Ord
,那麼一定要先推導出 Eq
才行。畢竟沒有辦法確定相等與否的東西,那就更談不上什麼比較大小了。
而這麼一來,我們就可以知道在之前為了要讓溫度能夠印在畫面上的那個 deriving (Show)
的意思,就是讓 Haskell 去推導出怎麼把這個型別轉換成字串印在畫面上了。
但用自動推導的方式,會讓我們的在比較一個攝氏溫度與一個華氏溫度時永遠回傳 False
。我們想要讓這個可以自動轉換後進行比較。那麼我們可以先宣告一個轉換用的函式,可以將攝氏溫度都轉換為華氏,而華氏溫度保持不變,我們就叫它 toF
吧:
-- Haskell 語法
toF temp =
case temp of
Celsius t -> Fahrenheit (t * 9 / 5 + 32)
_ -> temp
如果用 ghci
來查的話,會看到實作每個 typeclass 最少需要哪些函式,而其餘這個 typeclass 可用的函式, Haskell 會自動幫你推導出來。例如 Eq
:
-- Haskell 語法
*Main> :i Eq
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
{-# MINIMAL (==) | (/=) #-}
-- Defined in ‘GHC.Classes’
倒數第二行寫的 MINIMAL,就是最少需要實作的函式。而 (==) | (/=)
,就表示你只要實做 ==
(相等) 或是 /=
(不相等) 其中一個函式就好,而另一個就能夠自動推導出來。
又或如 Ord
:
-- Haskell 語法
class Eq a => Ord a where
compare :: a -> a -> Ordering
(<) :: a -> a -> Bool
(<=) :: a -> a -> Bool
(>) :: a -> a -> Bool
(>=) :: a -> a -> Bool
max :: a -> a -> a
min :: a -> a -> a
{-# MINIMAL compare | (<=) #-}
-- Defined in ‘GHC.Classes’
就表示一旦實做了 compare
或是 <=
(小於等於) 後,就會把剩下的大於、小於、max
等等推導出來。
接著就可以來實作 Temp 這個型別的 Eq
及 Ord
typeclass 了。另外要記得把原本自動推導的 Eq
及 Ord
拿掉。
-- Haskell 語法
data Temp = Celsius Float | Fahrenheit Float
deriving (Show)
toF temp =
case temp of
Celsius t -> Fahrenheit (t * 9 / 5 + 32)
_ -> temp
instance Eq Temp where
Fahrenheit t1 == Fahrenheit t2 = t1 == t2
t1 == t2 = (toF t1) == (toF t2)
instance Ord Temp where
Fahrenheit t1 <= Fahrenheit t2 = t1 <= t2
t1 <= t2 = (toF t1) <= (toF t2)
---- 用看看
temp1 = Celsius 50
temp2 = Fahrenheit 122
temp3 = Fahrenheit 100
temp1 == temp2 -- => True
temp1 < temp3 -- => False
<應該還要有故事但如你所見我的哏基本上用完了還在生>
[to be continue]