在〈Haskell Tutorial(27)可直接函式套用的 Applicative〉中看過,List 在實現 Applicative
的行為時,實際上隱藏了 List Comprehension 的細節,因此,對於 (+) <$> [1, 2, 3] <*> [4, 5, 6]
,你得到的結果與 [x + y | x <- [1, 2, 3], y <- [4, 5, 6]]
相同。
然而,對於 List 還有另一個常見的操作,那就是將兩個 List 中的元素,兩兩配對進行某個運算。例如 zipWith (+) [1, 2, 3] [4, 5, 6]
,你會得到 [5, 7, 9]
的結果,因為 +
是以 [1 + 4, 2 + 5, 3 + 6]
的方式套用。
如果你希望 List 有另一個 Applicative
的實現,對於 (+) <$> [1, 2, 3] <*> [4, 5, 6]
可以得到 [5, 7, 9]
,其實是沒辦法直接做到的,因為一個型態在實現一個 Typeclass,只能有一個實作,你需要有一個「新型態」來代表 List,然後用新型態來實現 Applicative
的行為。
為型態建立 newtype
Haskell 中可以使用 newtype
為某型態建立新型態,就方才我們的需求來說,我們需要有個新型態 ZipLt a
來代表 [a]
,這時可以如下定義:
newtype ZipLt a = ZipLt [a] deriving Show
這麼一來,對於 [Integer]
,就可以使用 ZipLt Integer
來代表它:
因此,你可以使用 ZipLt
來實現 Functor
與 Applicative
的行為:
import Control.Applicative
newtype ZipLt a = ZipLt [a] deriving Show
instance Functor ZipLt where
fmap f (ZipLt xs) = ZipLt $ map f xs
instance Applicative ZipLt where
pure x = ZipLt (repeat x)
ZipLt fs <*> ZipLt xs = ZipLt (zipWith (\f x -> f x) fs xs)
對於 Functor
的實作,其實沒什麼意思,就只是使用 ZipLt
來代表 map
的結果,實現它的目的,只是因為一個 Applicative
得是一個 Functor
,如果你不想親自動手做這件事,那可以在編譯時加上 -XDeriveFunctor
,然後令 ZipLt
自動衍生 Functor
:
newtype ZipLt a = ZipLt [a] deriving (Show, Functor)
ZipLt
在實現 Applicative
的 pure
時,就只是產生無限長的 List,當中是指定的函式罷了,由於惰性的關係,repeat
只會在真正需要時才會產出下一個元素,因此,實際上最後總共產生的 fs
長度,會是與 xs
的長度相同的。
現在,對於 (+) <$> ZipLt [1, 2, 3] <*> ZipLt [4, 5, 6]
,會產生 ZipLt [5,7,9]
的結果,實際上,Control.Applicative
模組中,就定義有 ZipList
,並實現了 Applicative
的行為:
ZipList
的定義大致上是:
newtype ZipList a = ZipList { getZipList :: [a] }
data 與 newtype
看起來,newtype
似乎與 data
相似,它可以定義值建構式,可以衍生,也可以使用 Record 語法,實際上,上面的例子中,將 newtype
改成 data
也行得通,兩者都可以用來為既有的型態建立一個新型態,那麼,data
與 newtype
有什麼樣的不同呢?
就語法上來說,newtype
的限制是只能有一個值建構式,而當中只能有一個欄位,當你發現使用 data
定義只用到一個值建構式且當中為單一欄位時,其實表示新型態與欄位中的型態是直接對應的,也就是說,兩者是等價的,使用 data
定義只用到一個值建構式且當中為單一欄位時,可以考慮將 data
改為 newtype
。
這樣的好處是,newtype
對編譯器來說,仍是建立一個新型態,這個新型態與值建構式型態(記得嗎?值建構式是個函式,它有型態)只提供編譯器檢查之用,實際上不會真正有值建構式的包裹與拆解負擔,在執行時期,新型態與欄位的型態仍視為相同型態。
這會產生有趣的結果,舉例來說:
data MyInt = CMyInt Int
showMyInt :: MyInt -> Int
showMyInt (CMyInt _) = 100
main = print $ showMyInt undefined
其中 undefined
的定義是:
undefined :: a
undefined = error "Prelude.undefined"
程式可以通過編譯,然而執行時期會嘗試使用值建構式拆解 undefined
傳回值,因而發生例外。
如果將 data
改為 newtype
,程式也可以通過編譯,也可以順利執行而顯示 100
的結果,這是因為實際上不會有任何 MyInt
型態的值是經由 CMyInt
建構而成,也不會有執行時期嘗試使用值建構式拆解 undefined
傳回值的問題,undefined
根本不會被執行,因而不會有例外產生。
type 與 newtype
你可能會想到另一個關鍵字 type
,type
並不會建立新型態,單純只是為型態建立一個別名,就像 type String = [Char]
,無論你在型態宣告上使用 String
或 [Char]
,編譯器都視為相同型態。
因此可以這麼區分,data
定義的新型態與欄位型態,編譯時期與執行時期都是不同型態,newtype
定義的新型態,只在編譯時期視為不同型態,而 type
只是別名,單純用來改善型態的可讀性,無法提供編譯器進行型態檢查。
最後來出個題目,以下這個程式可以通過編譯:
data MyInt = CMyInt Int
getInt :: MyInt -> IO Int
getInt (CMyInt i) = do
print "getInt"
return i
main = do
i <- getInt undefined
print i
不過執行時會發生例外:
將 data
改成 newtype
,程式也可以通過編譯,執行時一樣也是會發生例外:
不過仔細一看有點不同,這次是顯示了 "getInt"
才發生例外,思考一下,這是為什麼呢?