跳轉到內容

另一個 Haskell 教程/型別進階

來自華夏公益教科書
Haskell
另一個 Haskell 教程
前言
介紹
入門
語言基礎 (解答)
型別基礎 (解答)
IO (解答)
模組 (解答)
高階語言 (解答)
高階型別 (解答)
單子 (解答)
高階 IO
遞迴
複雜度

正如你可能已經從這一點上得知,型別系統是 Haskell 不可分割的一部分。雖然本章稱為“高階型別”,但你可能會發現它比這更普遍,並且你不應該僅僅因為你不感興趣型別系統而跳過它。


類型別名

[編輯 | 編輯原始碼]

類型別名在 Haskell 中僅僅是為了方便:它們的移除不會使 Haskell 變得不那麼強大。

考慮你一直在處理三維點列表的情況。例如,你可能有一個型別為 [(Double,Double,Double)] -> Double -> [(Double,Double,Double)] 的函式。因為你是一位優秀的軟體工程師,你希望在所有頂級函式上放置型別簽名。但是,不斷鍵入 [(Double,Double,Double)] 會變得非常乏味。為了解決這個問題,你可以定義一個類型別名

type List3D = [(Double,Double,Double)]

現在,你函式的型別簽名可以寫成 List3D -> Double -> List3D

我們應該注意類型別名不能是自引用的。也就是說,你不能有

type BadType = Int -> BadType

這是因為這是一個“無限型別”。由於 Haskell 非常早地消除了類型別名,所以任何 BadType 的例項都將被替換為 Int -> BadType,這將導致無限迴圈。

為了建立一個遞迴型別,可以使用newtype

   newtype GoodType =  MakeGoodType (Int -> GoodType)


類型別名也可以是引數化的。例如,你可能希望能夠更改列表中 3D 點的型別。為此,你可以定義

type List3D a = [(a,a,a)]

然後你對 [(Double,Double,Double)] 的引用將變成 List3D Double



新型別

[編輯 | 編輯原始碼]

考慮你需要有一個與 Int 非常相似的型別,但其排序定義不同。也許你希望先按偶數再按奇數對 Int 進行排序(也就是說,所有奇數都大於任何偶數,並且在奇數/偶數子集中,排序是標準的)。

不幸的是,你不能為 Int 定義一個新的 Ord 例項,因為那樣 Haskell 就不知道要使用哪一個。你想要的是定義一個與 Int 同構 的型別。

注意

“同構”是數學中的一個常用術語,基本上意味著“結構相同”。例如,在圖論中,如果你有兩個圖除了節點上的標籤不同之外完全相同,那麼它們就是同構的。在我們的上下文中,如果兩個型別具有相同的底層結構,那麼它們就是同構的。

一種方法是定義一個新的資料型別

data MyInt = MyInt Int

然後我們可以為這個資料型別編寫適當的程式碼。問題(非常微妙)是這個型別並不真正與 Int 同構:它有一個額外的值。當我們想到 Int 型別時,我們通常認為它接受所有整數值,但它實際上還有一個額外的值:(讀作“底部”),用於表示錯誤或未定義的計算。因此,MyInt 不僅具有 MyInt 0MyInt 1 等值,而且還具有 MyInt 。但是,由於資料型別本身可以是未定義的,因此它還有一個額外的值:,它不同於 MyInt ,這使得型別不同構。(有關底部的更多資訊,請參閱關於 底部 的部分。)

忽略那個微妙之處,這種表示可能存在效率問題:現在,我們不再只是儲存一個整數,而是必須儲存一個指向整數的指標,並且每當我們需要 MyInt 的值時,都必須跟蹤該指標。

為了解決這些問題,Haskell 有一個newtype構造。一個newtype介於資料型別和類型別名之間:它像資料型別一樣有一個建構函式,但它只能有一個建構函式,並且這個建構函式只能有一個引數。例如,我們可以定義

newtype MyInt = MyInt Int

但我們不能定義任何

newtype Bad1 = Bad1a Int | Bad1b Double
newtype Bad2 = Bad2 Int Double

當然,我們不能像上面那樣定義 Bad2 並不是什麼大問題:我們只需使用type代替

type Good2 = Good2 Int Double

或者(幾乎等效地)宣告一個指向現有元組型別的新類型別名

newtype Good2 = Good2 (Int,Double)

現在,假設我們已經定義了 MyInt 作為newtype:

instance Ord MyInt where
  compare (MyInt i) (MyInt j)
    | odd  i && odd  j = compare i j
    | even i && even j = compare i j
    | even i           = LT
    | otherwise        = GT

像資料型別一樣,我們仍然可以在新型別上派生類,如 ShowEq(事實上,我隱式地假設我們在 MyInt 上派生了 Eq——我上面的程式碼中的假設在哪裡?)。

此外,在 GHC 的最新版本中(請參閱關於 Ghc 的部分),在新型別上,你可以派生基型別(在本例中為 Int)是例項的任何類。例如,我們可以為 MyInt 派生 Num,以便在它上面提供算術函式。

在新型別上進行模式匹配與資料型別完全相同。我們可以為 MyInt 編寫建構函式和解構函式,如下所示

mkMyInt i = MyInt i
unMyInt (MyInt i) = i



資料型別

[編輯 | 編輯原始碼]

我們已經看到資料型別在各種情況下被使用。本節總結了其中一些討論,並介紹了 Haskell 中一些常見的資料型別。它還提供了對資料型別究竟是什麼的更理論上的基礎。


嚴格欄位

[編輯 | 編輯原始碼]

Haskell 的一大優點是計算是惰性執行的。但是,有時會導致效率低下。解決此問題的一種方法是使用具有嚴格欄位的資料型別。在我們討論解決方案之前,讓我們花一些時間來更熟悉底部是如何融入這幅圖景的(有關更多理論,請參閱關於 底部 的部分)。

假設我們定義了單位資料型別(這是你可以定義的最簡單的資料型別之一)

data Unit = Unit

此資料型別只有一個建構函式,Unit,它不接受任何引數。在像 ML 這樣的嚴格語言中,Unit 型別只有一個值:即 Unit。在 Haskell 中並非如此。實際上,Unit 型別有*兩個*值。其中一個是 Unit。另一個是底部(寫為 )。

您可以將底部視為表示不會停止的計算。例如,假設我們定義了值

foo = foo

這完全有效的 Haskell 程式碼,只是說當您要計算 foo 時,您只需計算 foo 即可。顯然,這是一個“無限迴圈”。

foo 的型別是什麼?僅僅是 a。我們不能說比這更多。實際上,foo 的型別為 a 的事實告訴我們它必須是一個無限迴圈(或其他一些奇怪的值)。但是,由於 foo 的型別為 a,因此可以具有任何型別,它也可以具有 Unit 型別。例如,我們可以寫

foo :: Unit
foo = foo

因此,我們找到了第二個具有 Unit 型別的 value。實際上,我們已經找到了所有具有 Unit 型別的 value。任何其他非終止函式或產生錯誤的函式都將與 foo 產生完全相同的效果(儘管 Haskell 透過 error 函式提供了一些額外的實用程式)。

這意味著,例如,實際上有*四個*具有 Maybe Unit 型別的 value。它們是:NothingJust Just Unit。但是,可能是您作為程式設計師知道您永遠不會遇到其中第三個的情況。也就是說,您希望 Just 的引數是*嚴格*的。這意味著如果 Just 的引數是底部,則整個結構將成為底部。您使用感嘆號來指定建構函式為嚴格的。我們可以定義 Maybe 的嚴格版本為

data SMaybe a = SNothing | SJust !a

現在只有三個 SMaybe 值。我們可以透過編寫以下程式來檢視區別

module Main where

import System

data SMaybe a = SNothing | SJust !a  deriving Show

main = do
  [cmd] <- getArgs
  case cmd of
    "a" -> printJust   undefined
    "b" -> printJust   Nothing
    "c" -> printJust  (Just undefined)
    "d" -> printJust  (Just ())

    "e" -> printSJust  undefined
    "f" -> printSJust  SNothing
    "g" -> printSJust (SJust undefined)
    "h" -> printSJust (SJust ())

printJust :: Maybe () -> IO ()
printJust Nothing = putStrLn "Nothing"
printJust (Just x) = do putStr "Just "; print x

printSJust :: SMaybe () -> IO ()
printSJust SNothing = putStrLn "Nothing"
printSJust (SJust x) = do putStr "Just "; print x

在這裡,根據傳遞的命令列引數,我們將執行不同的操作。各種選項的輸出為

示例

% ./strict a
Fail: Prelude.undefined

% ./strict b
Nothing

% ./strict c
Just
Fail: Prelude.undefined

% ./strict d
Just ()

% ./strict e
Fail: Prelude.undefined

% ./strict f
Nothing

% ./strict g
Fail: Prelude.undefined

% ./strict h
Just ()

這裡值得注意的是案例“c”和“g”之間的區別。在案例“c”中,列印了 Just,因為這是在評估未定義的值*之前*列印的。但是,在案例“g”中,由於建構函式是嚴格的,因此只要您匹配 SJust,您也會匹配 value。在這種情況下,value 未定義,因此整個操作在有機會做*任何事*之前就失敗了。





我們已經遇到過幾次型別類,但僅在先前存在的型別類的上下文中。本節討論如何定義自己的型別類。我們將從 Pong 開始討論,然後繼續討論計算的有用泛化。

這裡的討論將由 Pong 遊戲的構建所驅動(有關完整程式碼,請參見關於 Pong 的附錄)。在 Pong 中,螢幕上繪製了三件事:兩個球拍和球。雖然球拍和球在某些方面有所不同,但它們有很多共同點,例如位置、速度、加速度、顏色、形狀等等。我們可以透過為 Pong 實體定義一個類來表達這些共同點,我們稱之為 Entity。我們如下定義此類

class Entity a where
    getPosition :: a -> (Int,Int)
    getVelocity :: a -> (Int,Int)
    getAcceleration :: a -> (Int,Int)
    getColor :: a -> Color
    getShape :: a -> Shape

此程式碼定義了一個型別類 Entity。此類有五個方法:getPositiongetVelocitygetAccelerationgetColorgetShape,它們具有相應的型別。

這裡的第一行使用關鍵字class來引入一個新的型別類。我們可以將此型別類定義解讀為“有一個型別類‘Entity’;如果型別 ‘a’ 提供以下五個函式,則它就是 Entity 的例項:...”。要了解如何編寫此類的例項,讓我們定義一個玩家(球拍)資料型別

data Paddle =
   Paddle { paddlePosX, paddlePosY,
            paddleVelX, paddleVelY,
            paddleAccX, paddleAccY :: Int,
            paddleColor :: Color,
            paddleHeight :: Int,
            playerNumber :: Int }

有了此資料宣告,我們可以將 Paddle 定義為 Entity 的例項

instance Entity Paddle where
  getPosition p = (paddlePosX p, paddlePosY p)
  getVelocity p = (paddleVelX p, paddleVelY p)
  getAcceleration p = (paddleAccX p, paddleAccY p)
  getColor = paddleColor
  getShape = Rectangle 5 . paddleHeight

類函式的實際 Haskell 型別都包含上下文 Entity a =>。例如,getPosition 的型別為 Entity a => a -> (Int,Int)。但是,事實證明,我們的許多例程將需要實體也是 Eq 的例項。因此,我們可以選擇將 Entity 設為 Eq 的子類:也就是說,您只能在已經是 Eq 例項的情況下才能成為 Entity 的例項。為此,我們將類宣告的第一行更改為

class Eq a => Entity a where

現在,為了將 Paddle 定義為 Entity 的例項,我們首先需要它們成為 Eq 的例項 - 我們可以透過派生類來實現這一點。

計算

[edit | edit source]

讓我們回顧一下我們最初定義 資料型別-maybe 部分中 Maybe 資料型別的動機。我們希望能夠表達函式(即計算)可能會失敗。

讓我們考慮在圖上執行搜尋的情況。讓我們稍微說一下以設定一個小型的相簿

data Graph v e = Graph [(Int,v)] [(Int,Int,e)]

Graph 資料型別接受兩個型別引數,它們對應於頂點和邊標籤。Graph 建構函式的第一個引數是頂點的列表(集合);第二個是邊的列表(集合)。我們將假設這些列表始終已排序,並且每個頂點都有唯一的 ID,並且任何兩個頂點之間最多隻有一條邊。

假設我們要搜尋兩個頂點之間的路徑。也許這兩個頂點之間沒有路徑。為了表示這一點,我們將使用 Maybe 資料型別。如果成功,它將返回遍歷的頂點列表。我們的搜尋函式可以這樣寫(幼稚地):

search :: Graph v e -> Int -> Int -> Maybe [Int]
search g@(Graph vl el) src dst
    | src == dst = Just [src]
    | otherwise  = search' el
    where search' [] = Nothing
          search' ((u,v,_):es)
              | src == u  =
                case search g v dst of
                  Just p  -> Just (u:p)
                  Nothing -> search' es
              | otherwise = search' es

此演算法的工作原理如下(嘗試閱讀):要在圖 g 中從 srcdst 搜尋,首先我們檢查它們是否相等。如果它們相等,我們就找到了路徑,只需返回微不足道的解即可。否則,我們要遍歷邊列表。如果我們遍歷邊列表並且邊列表為空,則說明我們失敗了,因此我們返回 Nothing。否則,我們正在檢視從 uv 的邊。如果 u 是我們的源,那麼我們會考慮這一步並遞迴地在圖中從 vdst 搜尋。如果失敗了,我們會嘗試其餘的邊;如果成功了,我們將當前位置放在找到的路徑之前並返回。如果 u 不是我們的源,則此邊無用,我們繼續遍歷邊列表。

此演算法很糟糕:也就是說,如果圖包含迴圈,它會無限迴圈。不過,目前它已經足夠了。請務必充分理解它:事情只會變得更加複雜。

現在,在某些情況下,Maybe 資料型別是不夠的:也許我們希望連同失敗一起包含錯誤訊息。我們可以定義一個數據型別來表達這一點,如下所示:

data Failable a = Success a | Fail String

現在,失敗附帶了一個失敗字串來表達出錯的原因。我們可以重新編寫搜尋函式以使用此資料型別

search2 :: Graph v e -> Int -> Int -> Failable [Int]
search2 g@(Graph vl el) src dst
    | src == dst = Success [src]
    | otherwise  = search' el
    where search' [] = Fail "No path"
          search' ((u,v,_):es)
              | src == u  =
                case search2 g v dst of
                  Success p -> Success (u:p)
                  _         -> search' es
              | otherwise = search' es

此程式碼是上述程式碼的直接翻譯。

還有另一種計算選項:也許我們不希望只得到一條路徑,而是希望得到所有可能的路徑。我們可以將其表達為返回頂點列表列表的函式。基本思想是一樣的

search3 :: Graph v e -> Int -> Int -> [[Int]]
search3 g@(Graph vl el) src dst
    | src == dst = [[src]]
    | otherwise  = search' el
    where search' [] = []
          search' ((u,v,_):es)
              | src == u  =
                   map (u:) (search3 g v dst) ++
                   search' es
              | otherwise = search' es

由於標準序言 map 函式,這裡程式碼變得更短了,但本質上是一樣的。

我們可以問自己,所有這些有什麼共同點,並試圖將這些共同點歸納到一個類中。本質上,我們需要一些方法來表示成功,也需要一些方法來表示失敗。此外,我們需要一種方法來組合兩個成功(在前兩種情況下,選擇第一個成功;在第三種情況下,將它們串在一起)。最後,我們需要能夠用一些新值來擴充套件之前的成功(如果有的話)。我們可以將所有這些都放入一個類中,如下所示

class Computation c where
    success :: a -> c a
    failure :: String -> c a
    augment :: c a -> (a -> c b) -> c b
    combine :: c a -> c a -> c a

在這個類宣告中,我們說如果c提供了四個函式:successfailureaugmentcombine,那麼它就是類Computation的一個例項。success函式接受一個型別為a的值並將其包裝在c中,表示一個成功的計算。failure函式接受一個String並返回一個表示失敗的計算。combine函式接受兩個先前的計算並生成一個新的計算,它是這兩個計算的組合。augment函式稍微複雜一些。

augment函式接受一些先前給定的計算(即,c a)和一個函式,該函式接受該計算的值(a)並返回一個b,並生成一個在該計算中的b。請注意,在我們目前的情況下,給出augment型別c a -> (a -> a) -> c a就足夠了,因為a始終是[Int],但我們這次為了通用性而將其更通用。

augment的工作原理最好用示例來解釋。我們可以定義MaybeFailable[]Computation的例項,如下所示

instance Computation Maybe where
    success = Just
    failure = const Nothing
    augment (Just x) f = f x
    augment Nothing  _ = Nothing
    combine Nothing y = y
    combine x _ = x

在這裡,成功用Just表示,而failure忽略其引數並返回Nothingcombine函式接受我們找到的第一個成功並忽略其餘部分。函式augment檢查我們之前是否成功(因此有一個Just東西),如果有,則將f應用於它。如果我們之前失敗了(因此有一個Nothing),我們忽略該函式並返回Nothing

instance Computation Failable where
    success = Success
    failure = Fail
    augment (Success x) f = f x
    augment (Fail s) _ = Fail s
    combine (Fail _) y = y
    combine x _ = x

這些定義很明顯。最後

instance Computation [] where
    success a = [a]
    failure = const []
    augment l f = concat (map f l)
    combine = (++)

在這裡,成功計算的值是一個包含該值的單例列表。失敗用空列表表示,要組合之前的成功,我們只需將它們連線起來。最後,增強計算相當於將函式對映到之前的計算列表並將其連線起來。我們將函式應用於列表中的每個元素,然後將結果連線起來。

使用這些計算,我們可以將所有上述版本的搜尋表達為

searchAll g@(Graph vl el) src dst
    | src == dst = success [src]
    | otherwise  = search' el
    where search' [] = failure "no path"
          search' ((u,v,_):es)
              | src == u  = (searchAll g v dst `augment`
                             (success . (u:)))
                            `combine` search' es
              | otherwise = search' es

在這裡,我們看到了類Computation中所有函式的使用。

如果你理解了關於計算的討論,那麼你的處境就非常好,因為你已經理解了單子的概念,這可能是Haskell中最難的概念。事實上,Computation類幾乎與Monad類完全相同,只是success被稱為returnfailure被稱為fail,而augment被稱為>>=(讀作“繫結”)。combine函式實際上不是單子所必需的,但在MonadPlus類中找到了,原因將在後面變得很清楚。

如果你沒有理解這裡的所有內容,請再次通讀一遍,然後等到本章Monads中對單子的正式討論。



例項

[edit | edit source]

我們已經看到了如何宣告一些簡單類的例項;讓我們在這裡考慮一些更高階的類。在Functor模組中定義了一個Functor類。

注意

"函子"這個名稱,就像"單子"一樣,來自範疇論。在那裡,函子就像一個函式,但它不是將元素對映到元素,而是將結構對映到結構。

函子類的定義如下

class Functor f where
    fmap :: (a -> b) -> f a -> f b

fmap的型別定義(更不用說它的名稱了)與列表上的map函式非常相似。事實上,fmap本質上是對map的推廣,適用於任意結構(當然,列表已經是Functor的例項)。然而,我們也可以定義其他結構為函子的例項。考慮以下用於二叉樹的資料型別

data BinTree a = Leaf a
               | Branch (BinTree a) (BinTree a)

我們可以立即確定BinTree型別本質上是將型別a提升為該型別的樹。有一個與這種提升自然相關的函子。我們可以寫出這個例項

instance Functor BinTree where
    fmap f (Leaf a) = Leaf (f a)
    fmap f (Branch left right) =
        Branch (fmap f left) (fmap f right)

現在,我們已經看到了如何透過使用deriving關鍵字使類似BinTree的東西成為Eq的例項,但在這裡我們將手動完成它。我們想使BinTree as成為Eq的例項,但顯然,除非a本身是Eq的例項,否則我們無法做到這一點。我們可以在例項宣告中指定這種依賴關係

instance Eq a => Eq (BinTree a) where
    Leaf a == Leaf b = a == b
    Branch l r == Branch l' r' = l == l' && r == r'
    _ == _ = False

這第一行可以讀作"如果aEq的例項,那麼BinTree a也是Eq的例項"。然後我們提供定義。如果我們不包含Eq a =>部分,編譯器會抱怨,因為我們試圖在第二行中對as使用==函式。

定義中的"Eq a =>"部分稱為"上下文"。我們應該注意到,對上下文和宣告中出現的內容有一些限制。例如,我們不允許在右側沒有型別建構函式的例項宣告。要了解原因,請考慮以下宣告

class MyEq a where
    myeq :: a -> a -> Bool

instance Eq a => MyEq a where
    myeq = (==)

就目前而言,這個定義似乎沒有問題。但是,如果在程式中的其他地方有以下定義

instance MyEq a => Eq a where
    (==) = myeq

在這種情況下,如果我們試圖確定某個型別是否為Eq的例項,我們可以將其簡化為試圖找出該型別是否為MyEq的例項,而這又可以簡化為試圖找出該型別是否為Eq的例項,等等。編譯器透過拒絕第一個例項宣告來保護自己不受這種情況的影響。

這通常被稱為封閉世界假設。也就是說,當我們寫下第一個定義時,我們假設不會出現第二個定義(或一些同樣邪惡的定義)。然而,這個假設是無效的,因為沒有什麼可以阻止第二個定義(或一些同樣邪惡的定義)。封閉世界假設也會在你遇到以下情況時咬你一口

class OnlyInts a where
    foo :: a -> a -> Bool

instance OnlyInts Int where
    foo = (==)

bar :: OnlyInts a => a -> Bool
bar = foo 5

我們再次做出了封閉世界假設:我們假設OnlyInts的唯一例項是Int,但沒有理由在其他地方定義另一個例項,破壞了我們對bar的定義。




種類

[edit | edit source]

讓我們花點時間思考一下Haskell中有哪些可用的型別。我們有簡單型別,比如IntCharDouble等等。然後我們有型別建構函式,比如Maybe,它接受一個型別(比如Char)並生成一個新的型別,Maybe Char。類似地,型別建構函式[](列表)接受一個型別(比如Int)並生成[Int]。我們還有更復雜的東西,比如->(函式箭頭),它接受兩個型別(比如IntBool)並生成一個新的型別Int -> Bool

從某種意義上說,這些型別本身也有型別。像Int這樣的型別具有一些基本型別。像Maybe這樣的型別具有一種型別,它接受某種基本型別並返回某種基本型別。等等。

談論型別的型別會變得笨拙且高度模稜兩可,所以我們稱型別的型別為"種類"。我們一直在稱之為"基本型別"的東西具有種類"*"。種類為*的東西是可能具有實際值的東西。還有一個單一的種類建構函式,->,我們可以用它來構建更復雜的種類。

考慮Maybe。它接受某種種類為*的東西並生成某種種類為*的東西。因此,Maybe的種類是* -> *。回想一下Datatypes-pairs部分中對Pair的定義

data Pair a b = Pair a b

在這裡,Pair是一個型別建構函式,它接受兩個引數,每個引數的種類都是*,並生成一個種類為*的型別。因此,Pair的種類是* -> (* -> *)。但是,我們再次假設結合律,所以我們只寫* -> * -> *

讓我們做一個稍微奇怪的資料型別定義

data Strange c a b =
    MkStrange (c a) (c b)

在我們分析Strange的種類之前,讓我們思考一下它做了什麼。它本質上是一個配對建構函式,儘管它不配對實際的元素,而是配對另一個建構函式中的元素。例如,將c視為Maybe。那麼MkStrangeMaybes的兩種型別ab配對起來。然而,c不必是Maybe,而是可以是[],或者許多其他東西。

不過,我們對c瞭解什麼?我們知道它必須具有種類* -> *。這是因為我們在右邊有c a。型別變數ab與以前一樣,每個都具有種類*。因此,Strange的種類是(* -> *) -> * -> * -> *。也就是說,它接受一個種類為* -> *的建構函式(c)以及兩個種類為*的型別,並生成一個種類為*的東西。

可能會出現一個問題,即我們如何知道a具有種類*而不是其他種類k。事實上,Strange的推斷種類是(k -> *) -> k -> k -> *。然而,這需要在種類級別上的多型性,這太複雜了,所以我們預設假設k = *

注意

GHC有一些擴充套件,允許你直接指定建構函式的種類。例如,如果你想要一個不同的種類,你可以明確地寫出它

data Strange (c :: (* -> *) -> *) a b = MkStrange (c a) (c b)

來賦予Strange一個不同的種類。

種類的表示法表明我們可以執行部分應用,就像我們可以對函式進行部分應用一樣。事實上,我們可以。例如,我們可以有

type MaybePair = Strange Maybe

毫不奇怪,MaybePair的種類是* -> * -> *

我們應該在這裡注意,以下所有定義都是可接受的

type MaybePair1     = Strange Maybe
type MaybePair2 a   = Strange Maybe a
type MaybePair3 a b = Strange Maybe a b

這些看起來都一樣,但就Haskell的型別系統而言,它們實際上並不相同。以下所有都是使用上述內容的有效型別定義

type MaybePair1a = MaybePair1
type MaybePair1b = MaybePair1 Int
type MaybePair1c = MaybePair1 Int Double

type MaybePair2b = MaybePair2 Int
type MaybePair2c = MaybePair2 Int Double

type MaybePair3c = MaybePair3 Int Double

但以下內容是無效

type MaybePair2a = MaybePair2

type MaybePair3a = MaybePair3
type MaybePair3b = MaybePair3 Int

這是因為雖然可以在資料型別上部分應用型別建構函式,但不能在型別同義詞上部分應用。例如,MaybePair2a無效的原因是,MaybePair2被定義為一個帶有一個引數的型別同義詞,而我們沒有給出任何引數。MaybePair3的無效定義也是如此。



類層次結構

[edit | edit source]

預設

[edit | edit source]

它是什麼?

華夏公益教科書