使用data关键字定义新数据类型
--file BookStore.hs
data BookInfo=Book Int String [String]
deriving(Show)
BookInfo是类型构造器,用于指代(refer)类型,由于类型名首字母必须大写,类型构造器的首字母必须大写;Book是值构造器,首字母必须大写,该类型的值由值构造器创建;之后的Int String [String]是类型的组成部分,是用于储存值的槽或域。BookInfo 类型包含的成分和一个 (Int, String, [String]) 类型的三元组一样,但它们类型不相同。
值构造器可以看作函数,如myInfo = Book 46512 "BookName" ["Author1","Author2"]
中,将Int String [String]三个类型的值应用到Book,创建了BookInfo类型的值。
类型构造器和值构造器的命名
类型构造器和值构造器是相互独立的。类型构造器只能出现在类型的定义,或者类型签名当中。而值构造器只能出现在实际的代码中。因此给类型构造器和值构造器赋予一个相同的名字实际上并不会产生任何问题。
类型别名
--file BookStore.hs
type CustomerID=Int
type ReviewBody=String
data BookReview= BookReview BookInfo CustomerID ReviewBody
deriving(Show)
type BookRecord=(BookInfo,BookReview)
使用类型别名,目的是为已存在的类型设置一个更具描述性的名字或为为啰嗦的类型设置一个更短的名字。类型别名只是为已有类型提供了一个新名字,创建值的工作还是由原来类型的值构造器进行。
代数数据类型
一个典型的例子是Bool类型
--file Bool.hs
data Bool=False | True
在BookStore.hs中自定义代数数据类型
--file BookStore.hs
type CardNumber=String
type CardHolder =String
type Adress =[String]
data BillingInfo = CraditCard CardNumber CardHolder Address
|CashOnDelivery
|Invoice CustomerID
deriving(Show)
一个代数类型可以有多于一个值构造器。当一个类型拥有一个以上的值构造器时,这些值构造器通常被称为“备选”(alternatives)或“分支”(case),同一类型的所有备选,创建出的的值的类型都是相同的;备选都可以接受任意个数的参数,不同备选之间接受的参数类型、个数不必相同。
代数类型与元组区别
使用代数数据类型可以区分结构相同的数据,而使用元组则不行,因为对于两个不同的代数数据类型来说,即使值构造器成分的结构和类型都相同,它们也是不同的类型,对于元组来说,只要元素的结构和类型都一致,那么元组的类型就是相同。
例如,在下面代码中,
data Cartesian2D=Cartesian2D Double Double
deriving(Eq,Show)
data Polar2D=Polar2D Double Double
deriving(Eq,Show)
Cartesian2D 和 Polar2D 两种类型的成分都是 Double 类型,但是,这些成分表达的是不同的意思。因此 Haskell 不允许混淆使用这两种类型。
模式匹配:处理代数数据类型
模式匹配的过程就像是逆转一个值的构造(construction)过程,因此它有时候也被称为解构(deconstruction)。
所谓处理需要做到两点:
- 如果这个类型有一个以上的值构造器,那么应该可以知道,这个值是由哪个构造器创建的
- 如果一个值构造器包含不同的成分,那么应该有办法提取这些成分
--file myNot.hs
myNot True=False
myNot False=True
这个例子中,并非同时定义两个myNot函数,而是Haskell 允许将函数定义为一系列等式: myNot 函数的两个等式分别定义了该函数对于输入参数在不同模式之下的行为。对于每行等式,模式定义放在函数名之后, = 符号之前。
将False应用到myNotmyNot False
的执行逻辑为,首先调用 myNot ,检查输入参数 False 是否和第一个模式的值构造器匹配 —— 不匹配,于是继续尝试匹配第二个模式 —— 匹配成功,于是第二个等式右边的值被作为结果返回。
-- file sumList.hs
sumList (x:xs)=x+sumList xs
sum []=0
匹配列表还可使用@语法sumList all@(x:xs)=x+sumList xs
,all
代表列表整体,对于此例还需要知道的一点是列表 [1, 2] 实际上只是 (1:(2:[])) 的一种简单的表示方式,(:) 用于构造列表
将[1,2]应用到sumListsumList [1,2]
后的执行逻辑为:
- [1, 2] 尝试对第一个等式的模式 (x:xs) 进行匹配,模式匹配成功, x 绑定为 1 ,xs绑定为 [2] ,此时,表达式就变为1 + (sumList [2])
- 递归调用 sumList,对 [2] 进行模式匹配,依然在第一个等式匹配成功, x 绑定为 2 ,而 xs 绑定为 [] ,此时,表达式变为 1 + (2 + sumList [])
- 递归调用 sumList ,输入为 [] ,第二个等式的 [] 模式匹配成功,返回 0 ,整个表达式为 1 + (2 + (0))
- 计算结果为 3
代数数据类型的匹配
代数数据类型的成分是匿名且按位置排序,这是说对成分的访问是通过位置来实行的。访问器的实现依赖代数数据类型的模式匹配,可以通过这个类型的值构造器来进行,如下
--file BookStore.hs
bookID (Book id _ _)=id
bookTitle (Book _ tiltle _)=tiltle
bookAuthors (Book _ _ authors)=authors
将book=(Book 3 "Probability Theory" ["E.T.H.Jaynes"])
应用到bookIDbookID book
可以得到id值3;如果在匹配模式中我们不在乎某个值的类型,那么可以用下划线字符 “_” (通配符)作为符号来进行标识,通配符并不会绑定成一个新的变量。使用通配符的好处体现在,如果在一个匹配模式中引入了一个变量,但没有在函数体中用到它的话,编译器会发出警告。
记录语法
使用记录语法可以在定义一种数据类型的同时,就可以定义好每个成分的访问器。
下面两段代码意义几乎完全一致
--原生语法
data Customer = Customer Int String [String]
deriving (Show)
customerID :: Customer -> Int
customerID (Customer id _ _) = id
customerName :: Customer -> String
customerName (Customer _ name _) = name
customerAddress :: Customer -> [String]
customerAddress (Customer _ _ address) = address
--记录语法
data Customer = Customer {
customerID :: CustomerID,
customerName :: String,
customerAddress :: [String]
} deriving (Show)
使用记录语法后既可以用值构造器的应用语法构造值,也可以使用标识法构造值
customer1 = Customer 271828 "J.R. Hacker" ["255 Syntax Ct","Milpitas, CA 95134","USA"]
customer2 = Customer {
customerID = 271828
, customerAddress = ["1048576 Disk Drive",
"Milpitas, CA 95134",
"USA"]
, customerName = "Jane Q. Citizen"
}
一点差别是,使用记录语法来定义类型时,该类型的打印格式会与原来不同。
参数化类型
--这段代码是不能在 ghci 里面执行的,它简单地展示了标准库是怎么定义 Maybe 这种类型的
data Maybe a = Just a
| Nothing
a是类型变量,Maybe 类型使用另一种类型作为它的参数,从而使得 Maybe 可以作用于任何类型的值。
递归类型
列表就是一种典型的递归类型
--file MyList.hs
data MyList a = Cons a (MyList a)
| Nil
deriving(Show)
--可以用下面的函数来验证MyList等价于List
fromList (x:xs)=Cons x (fromList xs)
fromList []=Nil
此例中的自定义列表MyList,其实就是用Cons替代(:),用Nil替代[],Cons 0 Nil 等价于(0:[])或[0]。
定义二叉树的递归类型如下,
--file Tree.hs
data Tree a=Node a (Tree a) (Tree a)
|Empty
deriving(Show)
simpleTree=Node "parent" (Node "left child" Empty Empty) (Node "right child" Empty Empty)
练习
- 写一个与 fromList 作用相反的函数:传入一个 MyList a 类型的值,返回一个 [a]
--file MyList.hs
fromMyList (Cons x xs)=(x:fromMyList xs)
fromMyList Nil=[]
- 定义只需要一个构造器的树类型。不用 Empty 构造器,用 Maybe 表示节点的子节点???
--file MyTree.hs
data MyTree a b=Node a b b
deriving(Show)
tree=Node "parent" (Just (Node "lc" Nothing Nothing)) (Just (Node "rc" Nothing Nothing))
控制过程
- 利用
error "ExceptionInfo"
终止程序 返回异常信息 - 用 Maybe 类型来表示有可能出现错误的情况,若想指出某个操作可能会失败,用 Nothing 构造器,反之用 Just 构造器。
safeSecond :: [a] -> Maybe a
safeSecond xs = if null (tail xs)
then Nothing
else Just (head (tail xs))
safeSecond [] = Nothing
--模式匹配的写法
tidySecond :: [a] -> Maybe a
--该模式匹配至少有两个元素的列表(因为它有两个列表构造器)
--并将列表的第二个元素的值绑定给 变量 x
tidySecond (_:x:_) = Just x
tidySecond _ = Nothing
- 引入局部变量
lend amount balance = let reserve = 100
newBalance = balance - amount
in if balance < reserve
then Nothing
else Just newBalance
lend2 amount balance = if amount < reserve * 0.5
then Just newBalance
else Nothing
where reserve = 100
newBalance = balance - amount
- let 表达式在函数体内部任何地方都可以引入局部变量
let 关键字标识一个变量声明区块的开始,用 in 关键字标识这个区块的结束;每行引入一个局部变量,这些变量既可在let定义区使用,也可以在 in 关键字的表达式中使用,=
左侧是变量名、右侧则是该变量绑定的表达式(之所以称变量名被绑定到了一个表达式而不是一个值,是由于 Haskell 是一门惰性求值的语言,变量名所对应的表达式一直到被用到时才会求值) - where 从句中的定义的局部变量在其跟随的主句中有效
4.局部函数
pluralise :: String -> [Int] -> [String]
pluralise word counts = map plural counts
where plural 0 = "no " ++ word ++ "s"
plural 1 = "one " ++ word
plural n = show n ++ " " ++ word ++ "s"
局部函数可以使用它所在的局部作用域内的任意变量:如上,使用了在外部函数 pluralise 中定义的变量 word。在 pluralise 的定义里,map 函数将局部函数 plural 逐一应用于 counts 列表的每个元素。
- Case:在表达式内部使用模式匹配
fromMaybe defval wrapped =
case wrapped of
Nothing -> defval
Just value -> value
- case 关键字后面可以跟任意表达式,对这个表达式进行匹配,of 关键字标识表达式结束及匹配开始。
- 一项匹配由三个部分组成:模式、箭头
->
、结果表达式;匹配按自上而下进行,如果模式匹配成功,则计算并返回对应表达式的结果;结果表达式应当是同一类型的。 - 如果使用通配符 _ 作为最后一个被匹配的模式,则意味着该模式是default模式
- Guards:条件求值
lend3 amount balance
| amount <= 0 = Nothing
| amount > reserve * 0.5 = Nothing
| otherwise = Just newBalance--otherwise 只是一个被绑定为值 True 的普通变量
where reserve = 100
newBalance = balance - amount
模式匹配加Guards的执行逻辑:
- 某个模式匹配成功,那么它后面的守卫将按照顺序依次被求值
- 如果某个守卫值为真,那么返回它所对应的函数体作为结果
- 如果所有的守卫值都为假,那么模式匹配继续进行,即尝试匹配下一个模式。
myDrop n xs = if n <= 0 || null xs
then xs
else myDrop (n - 1) (tail xs)
--模式匹配、Guards重构
niceDrop n xs | n <= 0 = xs
niceDrop _ [] = []
niceDrop n (_:xs) = niceDrop (n - 1) xs
练习
- 写一个用来计算列表元素的个数的函数,并添加函数的类型签名,保证其输出的结果和标准函数 length 保持一致
--file myfunc.hs
mylength1::[a]->Int
mylength1 xs=sum [1|_<-xs]
mylength2::[a]->Int
mylength2 (_:xs)=1+mylength2 xs
mylength2 []=0
- 写一个用来计算列表平均值的函数,即列表元素的总和除以列表的长度
--file myfunc.hs
ave::[Int]->Float--或(Fractional a1,Integral a2) => [a2] -> a1
ave xs=fromIntegral (sum xs)/fromIntegral (mylength1 xs)
- 写一个将列表变成回文序列的函数,再写一个判断列表是否是一个回文序列的函数
--file myfunc.hs
getBtseq xs=xs ++ reverse xs
isBtseq xs=(xs==reverse xs)
- 写一个用于排序一个包含许多列表的列表的函数,排序规则基于子列表的长度
--file myfunc.hs
--基于子列表长度冒泡排序
swaps::[[t]] -> [[t]]
swaps []=[]
swaps (x:[])=[x]
swaps (x1:x2:xs)
| length x1 > length x2 = x2:swaps(x1:xs)
| otherwise = x1:swaps(x2:xs)
bubbleSort::Eq t => [[t]] -> [[t]]
bubbleSort xs
| xs_==xs =xs
| otherwise = bubbleSort xs_
where xs_=swaps xs
--Data.List.sortBy :: (a -> a -> Ordering) -> [a] -> [a]
--sortList :: Foldable t => [t a] -> [t a]
--sortList=Data.List.sortBy (\xs1 xs2->if length xs1<length xs2 then LT else GT)
- 用一个分隔符将一个包含许多列表的列表连接在一起
--file myfunc.hs
intersparse::a -> [[a]] -> [a]
intersparse _ []=[]
intersparse _ (lst:[])=lst
intersparse s (lst:lsts)=lst++[s]++intersparse s lsts