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

型別_Haskell筆記 3

免費2018-05-04#Functional_Programming#Haskell type#Haskell自定义类型#Haskell内置类型#Haskell类型派生

Haskell 是靜態型別的,一切都有型別,並且在編譯時就要確定

一.內建型別

幾種常見的型別如下:

  • Int :有界整數,32 位元機器上的界限是 [-2147483648, 2147483647]

  • Integer :無界整數,內建的大數型別,效率不如 Int

  • Float :單精確度浮點數,6 位小數

  • Double :雙精確度浮點數,15 位小數

  • Bool :布林值,值為 True/False

  • Char :字元

  • Tuple :元組本身也是型別,只有 () 一個值

內建的無界整數讓大數運算變得非常方便,例如求 100 的階乘:

> product [1..100]
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

二.變數型別

name :: String
name = "ayqy"

:: 讀作「型別為」( has type ),告訴編譯器變數 nameString 型別(即 [Char] 型別)的

另外,型別的首字母都是大寫的

P.S. 雖然理論上很多場景不需要手動註明型別(編譯器會自己推斷),但實踐建議是至少給頂層變數/函式都標明型別,當然,全都標上肯定是好習慣,畢竟明確的型別能夠大大增強可讀性,具體見 Type signatures as good style

P.S. 可以透過 :browse <module> 命令列出指定模組所有的型別推斷,便於給現有程式碼補充型別

三.函式型別

一些常用函式的型別簽名如下:

show :: Show a => a -> String
odd :: Integral a => a -> Bool
fromIntegral :: (Num b, Integral a) => a -> b
(+) :: Num a => a -> a -> a
(++) :: [a] -> [a] -> [a]

其中, ::=> 之間的部分是型別約束(宣告型別變數), => 之後的部分是其型別。型別宣告中的小寫字母(例如 a )叫做型別變數,未加限定的型別變數(如 ++ 型別中的 a )相當於泛型,用到型別變數的函式稱之為多型函式

比如 show :: Show a => a -> String 的含義是 show 的型別是一個接受 Show 型別參數,傳回 String 的函式。 (+) :: Num a => a -> a -> a 表示 + 的型別是接受兩個 Num 型別參數,傳回 Num 的(柯里化)函式。而 (++) :: [a] -> [a] -> [a] 表示 ++ 的型別是接受兩個 List 參數,傳回另一個 List 的函式,這裡的 a 沒有限定型別,所以 List 裡的元素可以是任意型別

型別部分的 -> 讀作「對映到」( maps to ),如何理解?

函式的數學定義是定義域到值域的對映關係,所以 f = x -> y 對應的數學含義是 y = f(x) ,也就是說 x 對映到 y (的對映關係)就是 f ,輸入 x 傳回對應的 y

所以 a -> b -> c 表示一個輸入 a ,傳回函式 b -> c 的函式,繼續呼叫這個傳回函式,輸入 b 傳回對應的 c 。忽略柯里化特性的話,可以簡單理解為接受兩個參數 a, b ,傳回 c

四.Typeclass

(==) :: Eq a => a -> a -> Bool

其中, Eq 被稱為 typeclass,相當於 interface,即定義了該型別成員必須具有的行為

除函式外的所有型別都屬於 Eq ,都可以判斷相等性。另一些常見的 typeclass 如下:

  • Ord :可以比較大小(能夠透過 <, >, <=, >= 等函式來比較大小,所以 Ord 一定屬於 Eq

  • Show :可用字串表示(除函式外,都是可 Show 的)。可以透過 show 函式把其他型別轉字串

  • Read :與 Show 相反。可以透過 read 函式把字串轉到其他型別

  • Enum :可列舉,即連續的。包括 ()BoolCharOrderingIntIntegerFloatDouble ,這些型別都可以用於 Range,可以透過 succpred 函式存取該型別值的後繼和前驅

  • Bounded :有明確的上下界。可以透過 maxBoundminBound 取指定型別的上下界(如 maxBound :: Int

  • Num :數值。成員都具有數值的特徵

  • Integral :整數。包括 IntInteger

  • Floating :小數。包括 FloatDouble

數字轉換的話,大範圍轉小範圍能夠隱式完成(如 NumFloat ),小轉大則需要透過 fromIntegral :: (Num b, Integral a) => a -> b 之類的函式來完成,常見的場景是 length 函式:

> length "Hello" + 0.5

<interactive>:191:18: error:
    ? No instance for (Fractional Int) arising from the literal ‘0.5’
    ? In the second argument of ‘(+)’, namely ‘0.5’
      In the expression: length "Hello" + 0.5
      In an equation for ‘it’: it = length "Hello" + 0.5

因為 length :: Foldable t => t a -> Int ,而 IntFractional 無法直接相加,所以需要這樣做:

> (fromIntegral (length "Hello")) + 0.5
5.5

另外, read 函式也很有意思,例如:

> read "12" + 4
16
> 1 : read "[2, 4]"
[1,2,4]

會根據上下文推斷出目標型別,所以如果沒有上下文就無法推斷:

> read "12"
*** Exception: Prelude.read: no parse

編譯器不知道我們想要什麼型別,可以手動宣告型別給點提示:

> read "12" :: Int
12
> read "12" :: Float
12.0

五.自定義型別

代數資料型別

Algebraic Data Type ,是指透過代數運算構造出來的資料結構,其中代數運算有兩種:

  • sum:邏輯或,例如 Maybe 型別的可能值之間是邏輯或關係

  • product:邏輯與,例如元組分量之間是邏輯與的關係

例如:

-- 邏輯與,Pair 型別是 Int-Double 對兒
data Pair = P Int Double
-- 邏輯或,Pair 型別個數值,要麼是 Int,要麼是 Double
data Pair = I Int | D Double

透過邏輯或和邏輯與能造出來任意複雜的資料結構,都可以稱為代數資料型別

從地位來看,代數資料型別之於函式式語言,就像代數之於數學,是非常基礎的東西。同樣,要進行代數運算,先要有數的定義:

[caption id="attachment_1705" align="alignnone" width="625"]map algebraic data type to math map algebraic data type to math[/caption]

宣告

透過 data 關鍵字來宣告自定義型別:

data Shape = Circle Float Float Float | Rectangle Float Float Float Float

表示 Shape 型別有 2 個值構造器( Circle, Rectangle ),即 Shape 型別的值是 Circle 或者 Rectangle值構造器本質上是函式

Circle :: Float -> Float -> Float -> Shape
Rectangle :: Float -> Float -> Float -> Float -> Shape

值構造器的參數(比如 CircleFloat Float Float )也被稱為項(field),實際上就是參數

既然值構造器是函式,那麼模式匹配也可以用於自定義型別:

circleArea (Circle _ _ r) = pi * r ^ 2
> circleArea (Circle 1 1 1)
3.1415927

求面積函式的型別為:

circleArea :: Shape -> Float

參數型別是 Shape ,而不是 Circle ,因為後者只是值構造器,並不是型別

另外,模式匹配都是針對值構造器的,常見的如 [] , otherwise/Ture , 5 等都是無參值構造器

遞迴定義型別

如果一個型別的值構造器的參數(field)是該型別的,那就產生遞迴定義了

例如 List 的語法糖:

[1, 2, 3]
-- 等價於(: 右結合,括號非必須)
1 : (2 : (3 : []))

就是一種遞迴定義:List 是把首項插入到剩餘項組成的 List 左側

不妨手搓一個:

infixr 5 :>
data MyList a = MyEmptyList | a :> (MyList a) deriving (Show)

其中,自定義運算子 :> 相當於 : ,都屬於值構造器(所以 x:xs 的模式匹配實際上是針對 List 的值構造器 : 的)。試玩一下:

> :t MyEmptyList
MyEmptyList :: MyList a
> 3 :> 5 :> MyEmptyList
3 :> (5 :> MyEmptyList)
> :t 3 :> 5 :> MyEmptyList
3 :> 5 :> MyEmptyList :: Num a => MyList a

除了語法上的差異,和 List 定義( 3 : 5 : [] )基本一致。再造幾個 List 特色函式:

_fromList [] = MyEmptyList
_fromList (x:xs) = x :> (_fromList xs)
_map f MyEmptyList = MyEmptyList
_map f (x :> xs) = f x :> _map f xs

繼續試玩:

> _fromList [1, 2, 3]
1 :> (2 :> (3 :> MyEmptyList))
> _map (+ 1) (_fromList [1, 2, 3])
2 :> (3 :> (4 :> MyEmptyList))

派生

只有 Show 類別(typeclass)的成員才能在 GHCi 環境直接輸出(因為輸出前呼叫 show :: Show a => a -> String ),所以,讓 Shape 成為 Show 的成員:

data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)

透過 deriving 關鍵字宣告型別派生,讓一個型別的值也成為其他型別的成員。試著直接輸出 Shape 值:

> Circle 1 1 1
Circle 1.0 1.0 1.0

乾脆把座標點也抽離出來:

data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float | Rectangle Point Point deriving (Show)
circleArea (Circle _ r) = pi * r ^ 2

Show 外,其他幾個能夠自動添上預設行為的 typeclass 是 Eq, Ord, Enum, Bounded, Read 。比如派生自 Eq 後可以透過 ==/= 來比較值的相等性:

data Mytype = Mytype Int String deriving (Show, Eq)
> Mytype 3 "a" == Mytype 4 "b"
False
> Mytype 3 "a" == Mytype 3 "a"
True

實際上,派生自 Eq 時自動添的相等性判斷就是檢查輸入參數是否一致:

1. 檢查值構造器是否一致
2. 檢查值構造器的參數是否一致

當然,要求參數也必須是 Eq 類別成員,否則無法自動比較(如果不滿足,就會拋個錯出來

ShowRead 也類似,用來完成字串與值之間的互相轉換:

data Mytype = Mytype Int String deriving (Show, Eq, Read)
> Mytype 3 "a"
Mytype 3 "a"
> read "Mytype 3 \"a\"" :: Mytype
Mytype 3 "a"

Ord 很有意思,表示成員是可排序的,但預設的排序依據如何確定呢?

data Mytype = EmptyValue | Singleton | Mytype Int String deriving (Show, Eq, Read, Ord)
> EmptyValue < Singleton
True
> Singleton < Mytype 3 "a"
True
> Mytype 3 "a" < Mytype 4 "a"
True

首先看型別宣告中的次序,或( | )在一起的,最先出現的值構造器,造出來的值最小,然後再按照類似的規則比較值構造器的參數,所以同樣要求參數都得是 Ord 成員

Enum, Bounded 用來定義列舉型別,即有限集合, Enum 要求每個值都有前驅 / 後繼,這樣就可以用於 Range 了, Bounded 要求值具有上下界,例如:

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Show, Bounded, Enum)
-- 上下界
> maxBound :: Day
Sunday
> minBound :: Day
Monday
-- 前驅/後繼
> pred Wednesday
Tuesday
> succ Wednesday
Thursday
-- Range
> [Wednesday ..]
[Wednesday,Thursday,Friday,Saturday,Sunday]

Record

對於簡單的資料型別,比如 Vector2D

data Vector2D = Vector2D Float Float deriving(Show)

簡單的 data 定義就能滿足語義需要(我們明確知道二維向量的兩個參數是橫縱座標),如果要描述的物件是複雜的東西,比如人有年齡、身高、體重、三圍:

data Person = Person Float Float Float Float Float Float deriving(Show)

這個看著太不直觀了,我們添行註解:

-- 年齡 身高 體重 上圍 中圍 下圍
data Person = Person Float Float Float Float Float Float deriving(Show)

想到了什麼?這不就是 10 幾個參數的函式嘛!參數巨多還要求順序,更麻煩的是,這是個資料型別,我們還需要一系列的 getter

getAge (Person age _ _ _ _ _) = age
getHeight (Person _ height _ _ _ _) = height
-- ...等等一堆 getter

其他語言裡一般怎麼處理這種情況?把零散參數組織起來(比如造個物件):

data Person = Person {
  age :: Float,
  height :: Float,
  weight :: Float,
  xw :: Float,
  yw :: Float,
  tw :: Float
} deriving (Show)

建立一個 person ,語義清楚,並且不用關心參數順序:

person =  Person {age=1, height=2, xw=4, yw=5, tw=6, weight=3}

會自動建立一堆 getter ,例如:

> :t age
age :: Person -> Float
> weight person
3.0

用起來比單純的型別定義方便多了

型別參數

型別構造器可以傳入參數,傳回新的型別。例如:

data Maybe a = Nothing | Just a

其中, a 是型別參數, Maybe 不是型別,而是型別構造器,具體的 Maybe xxx 才是型別, NothingJust xxx 都是該型別的值,例如:

Just 'a' :: Maybe Char
Nothing :: Maybe a

這樣做能夠得到一堆行為相似的型別,從應用場景上來看,帶參數的型別相當於泛型,是在具體型別之上的一層抽象,比如經典的 List

[1, 2, 3] :: Num t => [t]
"456" :: [Char]

都支援一些行為( Data.List 模組定義的各種函式):

map :: (a -> b) -> [a] -> [b]
> map (+ 1) [1, 2, 3]
[2,3,4]
> map (Data.Char.chr . (+ 1) . Data.Char.ord) "456"
"567"

length :: Foldable t => t a -> Int
> length [1, 2, 3]
3
> length "456"
3

maplength 函式並不關心 List a 具體型別是什麼,算是定義在抽象資料型別上的操作

Maybe 與 Either

data Maybe a = Nothing | Just a 	-- Defined in ‘GHC.Base’
data Either a b = Left a | Right b 	-- Defined in ‘Data.Either’

應用場景上, Maybe 用��表示可能出錯的結果,成功就是 Just a ,失敗就是 Nothing 。適用於單一錯誤原因的場景,比如 elemIndex

Data.List.elemIndex :: Eq a => a -> [a] -> Maybe Int

找到了傳回 Just Int 型別的下標,找不到就傳回 Nothing ,沒有第三種結果

單看異常處理的場景, Either 更強大一些,一般把失敗原因放到 Left a ,成功結果放到 Right b ,形式上與 Maybe 非常像,但 Left a 可以攜帶任意資訊,相比之下, Nothing 就太含糊了

P.S. JS 上下文中, Maybe 相當於約定成功就傳回值,失敗傳回 false ,只知道失敗了,可能不清楚具體原因。 Either 相當於約定回呼函式的第一個參數攜帶錯誤資訊,如果不為空就是失敗了,具體原因就是該參數的值

型別別名

Type synonyms(型別同義詞,即型別別名),之前已經見過了:

> :i String
type String = [Char] 	-- Defined in ‘GHC.Base’

透過 type 關鍵字給型別定義別名,讓 String 等價於 [Char] ,從而給型別宣告帶來語義上的好處,例如:

type PhoneNumber = String
type Name = String
type PhoneBook = [(Name,PhoneNumber)]

inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool
inPhoneBook name pnumber pbook = (name, pnumber) `elem` pbook

輸入姓名、電話和電話簿,傳回電話簿裡有沒有這條記錄。如果不起別名的話,型別宣告就只能是這樣:

inPhoneBook :: String -> String -> [(String, String)] -> Bool

當然,這個場景看起來似乎有些小題大做,為了一個沒什麼實際作用的東西(型別宣告)多做這麼多事情。但型別別名的特性是為了提供一種允許型別定義的語義更形象生動的能力,而不是針對具體某個場景,比如:

  • 讓型別宣告更加易讀

  • 替換掉那些重複率高的長名字型別 (如 [(String, String)] )

這種能力能夠讓型別對事物的描述更加明確

型別別名也可以有參數,比如,自定義的關聯列表:

type AssocList k v = [(k, v)]

允許任意 k-v ,保證其通用性,例如:

inPhoneBook :: (Eq k, Eq v) => k -> v -> AssocList k v -> Bool
inPhoneBook name pnumber pbook = (name, pnumber) `elem` pbook
> inPhoneBook 1 "1234" [(0, "0012"), (1, "123")]
False

此時 AssocList k v 對應的具體型別就是 AssocList Int String

> read "[(0, \"0012\"), (1, \"123\")]" :: AssocList Int String
[(0,"0012"),(1,"123")]

型別別名也有類似於柯里化的特性,例如:

type IntAssocList = Int
-- 等價於,保留一個參數
type IntAssocList v = Int v

如果參數給夠就是個具體型別,否則就是帶參數的型別

參考資料

評論

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

提交評論