另一種 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 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 0、MyInt 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
與資料型別一樣,我們仍然可以在新型別上派生類,如 Show 和 Eq(實際上,我隱式地假設我們在 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。它們是:,Nothing,Just 和 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。此類有五個方法:getPosition、getVelocity、getAcceleration、getColor 和 getShape,以及相應的型別。
這裡的第一行使用關鍵字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 中從 src 到 dst 進行搜尋,首先我們要檢查它們是否相等。如果它們相等,我們就找到了我們的路徑,只需返回微不足道的解決方案。否則,我們想要遍歷邊列表。如果我們正在遍歷邊列表,並且它為空,我們就失敗了,所以我們返回 Nothing。否則,我們正在檢視從 u 到 v 的一條邊。如果 u 是我們的源,那麼我們考慮這一步,並遞迴地搜尋從 v 到 dst 的圖。如果這失敗了,我們就嘗試剩餘的邊;如果這成功了,我們將當前位置放在找到的路徑之前並返回。如果 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是Computation類的例項,如果它提供四個函式:success、failure、augment和combine。success函式接受一個型別為a的值並將其包裝在c中,表示一個成功的計算。failure函式接受一個String並返回一個表示失敗的計算。combine函式接受兩個之前的計算並生成一個新的計算,它是兩個計算的組合。augment函式稍微複雜一些。
augment函式接受一些之前給定的計算(即c a)和一個函式,該函式接受該計算的值(a)並返回一個b,並在該計算中生成一個b。請注意,在我們當前的情況下,給augment型別c a -> (a -> a) -> c a就足夠了,因為a始終為[Int],但這次為了通用性,我們將其更一般化。
augment的工作原理最好透過例子來說明。我們可以定義Maybe、Failable和[]作為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忽略其引數並返回Nothing。combine函式接受我們找到的第一個成功並忽略其餘部分。函式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稱為return,failure稱為fail,augment稱為>>=(讀作“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
這第一行可以解讀為“如果a是Eq的例項,那麼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中有哪些型別。我們有簡單型別,例如Int、Char、Double等等。然後我們有型別建構函式,例如Maybe,它接受一個型別(例如Char)並生成一個新的型別Maybe Char。類似地,型別建構函式[](列表)接受一個型別(例如Int)並生成[Int]。我們有更復雜的東西,例如->(函式箭頭),它接受兩個型別(例如Int和Bool)並生成一個新的型別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。那麼MkStrange將Maybes的兩種型別a和b配對。但是,c不必是Maybe,而是可以是[],或者許多其他東西。
我們對c瞭解多少呢?我們知道它必須具有* -> *型別。這是因為在右側我們有c a。型別變數a和b都具有*型別,和之前一樣。因此,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]它是什麼?
