跳轉到內容

另一種 Haskell 教程/型別進階

來自 Wikibooks,開放世界中的開放書籍
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 GoodType =  MakeGoodType (Int -> GoodType)


類型別名也可以被引數化。例如,您可能希望能夠更改三維點列表中點的型別。為此,您可以定義

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

然後您對 [(Double,Double,Double)] 的引用將變為 List3D Double



新型別

[編輯 | 編輯原始碼]

考慮您需要有一個與 Int 非常類似的型別,但它的排序方式不同。也許您希望首先按偶數然後按奇數排序 Int(即所有奇數都大於任何偶數,並且在奇數/偶數子集中,排序是標準的)。

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

注意

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

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

data MyInt = MyInt Int

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

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

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

newtype MyInt = MyInt Int

但我們不能定義任何

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

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

type Good2 = Good2 Int Double

或(幾乎等效地)將新類型別名宣告為現有的元組型別

newtype Good2 = Good2 (Int,Double)

現在,假設我們已將 MyInt 定義為一個新型別:

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 的第二個值。事實上,我們已經找到了所有具有型別 Unit 的值。任何其他非終止函式或產生錯誤的函式將與 foo 產生完全相同的效果(儘管 Haskell 透過函式 error 提供了一些更多實用程式)。

這意味著,例如,實際上有 四個 值具有型別 Maybe Unit。它們是: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,你也就匹配了值。在這種情況下,該值是未定義的,因此整個操作在有機會執行 任何 操作之前就失敗了。





我們已經遇到過幾次型別類,但只是在之前存在的型別類的上下文中。本節介紹如何定義自己的型別類。我們將從談論乒乓球開始討論,然後繼續討論計算的有用泛化。

乒乓球

[編輯 | 編輯原始碼]

這裡的討論將以構建乒乓球遊戲為動力(有關完整程式碼,請參閱關於 乒乓球 的附錄)。在乒乓球中,螢幕上繪製了三樣東西:兩個球拍和一個球。雖然球拍和球在幾個方面有所不同,但它們有很多共同點,例如位置、速度、加速度、顏色、形狀等等。我們可以透過為乒乓球實體定義一個類來表達這些共同點,我們稱之為 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 的子類:也就是說,你只能成為 Entity 的一個例項,如果你已經是 Eq 的一個例項。要做到這一點,我們將類宣告的第一行更改為

class Eq a => Entity a where

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

讓我們回顧一下我們最初定義關於 資料型別-也許 的部分中的 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

在這個類宣告中,我們說cComputation類的例項,如果它提供四個函式:successfailureaugmentcombinesuccess函式接受一個型別為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 something),如果有,則將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類的所有函式的使用。

如果你理解了關於計算的討論,那麼你處於一個非常好的位置,因為你理解了monads的概念,這可能是Haskell中最難的概念。事實上,Computation類幾乎與Monad類完全相同,只是success稱為returnfailure稱為failaugment稱為>>=(讀作“bind”)。combine函式實際上不是monad所需要的,但它是在MonadPlus類中找到的,原因將在後面變得很明顯。

如果你沒有理解這裡的所有內容,請再次閱讀,然後等待本章中對monads的正式討論Monads.



例項

[edit | edit source]

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

注意

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

functor類的定義是

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

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

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

我們可以立即識別出BinTree型別本質上是將型別a提升到該型別的樹中。有一個自然相關的functor與這個提升相關聯。我們可以編寫這個例項

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的種類是* -> *。回想一下資料型別-對部分中對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]

它是什麼?

華夏公益教科書