跳到主要內容
黯羽輕揚每天積累一點點

newtype_Haskell 筆記 8

免費2018-06-08#Functional_Programming#Haskell type vs newtype vs data#Haskell newtype#Haskell类型别名#Haskell ZipList

為什麼需要 newtype?與 data、type 等關鍵字有什麼區別?

一.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 id liftA2 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 創建的新類型與原類型是完全不同的東西,唯一的聯繫是新類型內部實際操作的是原類型(通過持有原類型實例引用),通過這種方式在外層實現對原類型的擴展/增強

語法要求

從語法作用來看,newtypedata 一樣,都用來創建新類型,但 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 足夠聰明,明確知道不存在多個值構造器時,不再做無謂的計算

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論