iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
0
Software Development

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

mostly:functional 第十八章:不同事物的相同部份

我們在乎的,不是你的類別,而是你的型別。
用另一種方式講,我們不是那麼在乎你是哪一種人,而在乎你是什麼樣的人

-- 唐鳳, RubyConf 座談, 2014


-- 0610

自從上次從那個建築離開後,我又在這裡碰壁很久了。雖然有些東西看得比較清楚一點,但我發現就算我知道 immutable 跟遞迴什麼的,我在這裡能做的事,還是很少。

而所謂的碰壁,是字面上的意思。這個地方裡,有一大片區域被透明的牆所擋住,而許多看起來不斷在運算的東西,都連到牆的那一邊。而我怎樣都找不到方法或是線索過去。

我只注意到牆上寫了一個符號:>>=


-- 0630

我在周邊的屋子,找到許多其它人寫的手稿。畫有 >>= 那些手稿,都不斷的提到一個字:"Monad"。但是實在看不太懂。有些講了 flat_map,有些寫什麼「 Monad 只不過是自函子範疇上的么半群」之類的外星文,也有畫了一堆盒子的,還有一篇講什麼墨西哥捲餅的。


-- 0709

就在我決定先離開這座城去其它地方看看時,我在城的入口附近,發現有一塊金屬板浮在空中,上面泛著紫色的光澤。我摸了一下,板子上便開始浮現刻下的字。它寫著:

「From first principle」

「要理解 Monad,第一條建議,便是儘量不要去讀那些總結性的手稿。」好喔。

但正要往下看其它的文字時,金屬板便迅速飛離,還帶著發光的軌跡。這根本就奇幻小說作者找不到哏又想移動場景的手段啊,我想著,但還是跟了上去。軌跡停在一棟建築前面,上面寫著 class

class ?又不是物件導向,這東西怎麼會出現在這裡?




class is for the typeclass

在 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 這個型別的 EqOrd typeclass 了。另外要記得把原本自動推導的 EqOrd 拿掉。

-- 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]


上一篇
mostly:functional 第十七章:當我們談論容器時,我們在談論什麼?
下一篇
mostly:functional 第十九章:Semigroup 的法則
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35

尚未有邦友留言

立即登入留言