一.ZipList 與 List
在 List 場景,xs <*> ys 表示從左側 xs 中取出函數作用於右側 ys 中的每一項,有兩種實現方式:
-
笛卡爾積
-
拉鏈式的一一結對
分別對應 [] 和 ZipList,例如:
import Control.Applicative;
-- 笛卡爾積
> [(+2), (*2), (/2)] <*> [1, 2, 3]
[3.0,4.0,5.0,2.0,4.0,6.0,0.5,1.0,1.5]
-- 拉鏈式結對
> getZipList $ ZipList [(+2), (*2)] <*> ZipList [1..]
[3,4]
笛卡爾積只能用於有限長度 List,而拉鏈式結對還適用於無限長 List 的場景。對 <*> 而言,這兩種實現都是可取的,但 [] 無法同時擁有兩種不同的 Applicative 實現,所以造出了 ZipList,讓它以拉鏈結對的方式實現 Applicative
P.S.這裡提到的 <*> 是 Applicative 類定義的行為,具體見 [Functor 與 Applicative_Haskell 筆記 7](/articles/functor 與 applicative-haskell 筆記 7/)
二.newtype
ZipList 就是因這個場景而產生的,本質上是對 List 的包裝,定義如下:
newtype ZipList a = ZipList { getZipList :: [a] }
deriving ( Show, Eq, Ord, Read, Functor
, Foldable, Generic, Generic1)
(摘自 Control.Applicative)
通過 newtype 關鍵字,基於現有類型([])創建一個新的(ZipList),再重寫其接口實現:
instance Applicative ZipList where
pure x = ZipList (repeat x)
liftA2 f (ZipList xs) (ZipList ys) = ZipList (zipWith f xs ys)
P.S.這裡只實現了 liftA2,而沒有出現 <*>,是因為 Applicative 有最小完整定義(minimal complete definition)約束:
A minimal complete definition must include implementations of pure and of either <*> or liftA2. If it defines both, then they must behave the same as their default definitions:
(<*>) = liftA2 idliftA2 f x y = f <$> x <*> y
預先定義了這兩個函數的關聯,所以擇其一實現即可(根據關聯關係能夠自動生成另一個)
那麼,newtype 到底做了什麼?
實際上,newtype 所做的事情只是創建新類型,把現有類型包裝起來
在類似的場景下,JS 的話,我們會這麼做:
class ThisType {
constructor(value) {
this.value = value;
}
['<*>']() {
console.log('笛卡爾積');
}
};
class ThatType {
constructor(...args) {
this.originalValue = new ThisType(...args);
}
getOriginalType() {
return this.originalValue;
}
['<*>']() {
console.log('拉鏈結對');
}
};
// test
let thisOne = new ThisType(1);
thisOne['<*>'](); // 笛卡爾積
console.log(thisOne); // ThisType?{value: 1}
let thatOne = new ThatType(2);
thatOne['<*>'](); // 拉鏈結對
console.log(thatOne.getOriginalType()); // ThisType?{value: 2}
創建新類型(ThatType),把原類型(ThisType)包起來,提供不同的 <*> 實現
二者只是簡單的依賴,並沒有繼承關係,��以通過 newtype 創建的類型並不自動具有原類型的所有方法(也不會自動獲得原類型所實現的 typeclass)。就類型而言,二者是完全獨立的不同類型,所以:
> [3] ++ [1, 2]
[3,1,2]
> type IntList = [Int]
> [3] ++ ([1, 2] :: IntList)
[3,1,2]
> (ZipList [3]) ++ (ZipList [1, 2])
<interactive>:109:1: error:
? Couldn't match expected type '[a]'
with actual type 'ZipList Integer'
? In the first argument of '(++)', namely 'ZipList [3]'
...
不像 type 創建的別名類型可以與原類型等價換用,newtype 創建的新類型與原類型是完全不同的東西,唯一的聯繫是新類型內部實際操作的是原類型(通過持有原類型實例引用),通過這種方式在外層實現對原類型的擴展/增強
語法要求
從語法作用來看,newtype 與 data 一樣,都用來創建新類型,但 newtype 限制更多:
data can only be replaced with newtype if the type has exactly one constructor with exactly one field inside it.
要求 newtype 聲明的類型只能有一個值構造器,並且這個值構造器只能有一個參數(field)。除此之外,就與 data 關鍵字沒什麼區別了
P.S.關於值構造器與參數,見 [類型_Haskell 筆記 3](/articles/類型-haskell 筆記 3/#articleHeader7)
三.對比 type 和 data
| 關鍵字 | 作用 | 應用場景 |
|---|---|---|
data | 定義自己的(數據)類型 | 想要定義完全新的類型 |
type | 給現有類型起別名,得到的東西完全等價於原類型,可無條件換用/混用 | 想讓類型簽名更清楚(語義化)的時候 |
newtype | 將現有的類型包成一個新的類型,得到的類型與原類型不同,不能換用/混用 | 想讓現有類型具有一種不同的接口(typeclass)實現時 |
四.newtype 與惰性計算
Haskell 中大多數計算都是惰性的(少數指的是 foldl'、Data.ByteString 之類的嚴格版本),也就是說,計算只在不得不算的時候才會發生
惰性計算一般看起來都很符合直覺(不需要算的就先不算),但特殊的是,類型相關的場景存在隱式計算(不很符合直覺)
undefined
undefined 表示會造成錯誤的計算:
> undefined
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
undefined, called at <interactive>:12:1 in interactive:Ghci1
能夠用來檢驗惰性(計算到底執行了沒),例如:
> head [1, undefined, 3, undefined, undefined]
1
> let (a, _) = (1, undefined) in a + 1
2
特殊地,函數調用時的模式匹配本身是需要計算的,不管匹配結果是否需要用到,例如:
sayHello (_, _) = "hoho"
> sayHello undefined
"*** Exception: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
undefined, called at <interactive>:37:10 in interactive:Ghci17
而下面這種形式的就不會被計算:
sayHello _ = "hoho"
> sayHello undefined
"hoho"
二者的差異在於,對於前者,需要做一些基本的計算來看看應該用 Tuple 的哪個值構造器,後者則不需要
但奇怪的是,Tuple 明明只有一個值構造器(不需要「看應該用 Tuple 的哪個值構造器」):
data () = ()
我們知道沒必要去檢查應該用 Tuple 的哪個值構造器,但 Haskell 不知道,因為按照約定,data 關鍵字定義的數據類型可以有多個值構造器,即便只聲明了一個,它也要找過才知道。那麼,想到了什麼?
newtype。它明確約定了只有一個值構造器(並且這個值構造器只有一個參數),不妨試一下:
newtype MyTuple a b = MyTuple {getTuple :: (a, b)}
> sayHello (MyTuple _) = "hh"
> sayHello undefined
"hh"
確實如此,Haskell 足夠聰明,明確知道不存在多個值構造器時,不再做無謂的計算
暫無評論,快來發表你的看法吧