《Real World Haskell》笔记(3):定义类型并简化函数

使用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 xsall代表列表整体,对于此例还需要知道的一点是列表 [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)
练习
  1. 写一个与 fromList 作用相反的函数:传入一个 MyList a 类型的值,返回一个 [a]
--file MyList.hs
fromMyList (Cons x xs)=(x:fromMyList xs)
fromMyList Nil=[]
  1. 定义只需要一个构造器的树类型。不用 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))
控制过程
  1. 利用error "ExceptionInfo"终止程序 返回异常信息
  2. 用 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
  1. 引入局部变量
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 列表的每个元素。

  1. Case:在表达式内部使用模式匹配
fromMaybe defval wrapped =
    case wrapped of
      Nothing     -> defval
      Just value  -> value
  • case 关键字后面可以跟任意表达式,对这个表达式进行匹配,of 关键字标识表达式结束及匹配开始。
  • 一项匹配由三个部分组成:模式、箭头 ->、结果表达式;匹配按自上而下进行,如果模式匹配成功,则计算并返回对应表达式的结果;结果表达式应当是同一类型的。
  • 如果使用通配符 _ 作为最后一个被匹配的模式,则意味着该模式是default模式
  1. 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
练习
  1. 写一个用来计算列表元素的个数的函数,并添加函数的类型签名,保证其输出的结果和标准函数 length 保持一致
--file myfunc.hs
mylength1::[a]->Int
mylength1 xs=sum [1|_<-xs]

mylength2::[a]->Int
mylength2 (_:xs)=1+mylength2 xs
mylength2 []=0
  1. 写一个用来计算列表平均值的函数,即列表元素的总和除以列表的长度
--file myfunc.hs
ave::[Int]->Float--或(Fractional a1,Integral a2) => [a2] -> a1
ave xs=fromIntegral (sum xs)/fromIntegral (mylength1 xs)
  1. 写一个将列表变成回文序列的函数,再写一个判断列表是否是一个回文序列的函数
--file myfunc.hs
getBtseq xs=xs ++ reverse xs
isBtseq xs=(xs==reverse xs)
  1. 写一个用于排序一个包含许多列表的列表的函数,排序规则基于子列表的长度
--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)
  1. 用一个分隔符将一个包含许多列表的列表连接在一起
--file myfunc.hs
intersparse::a -> [[a]] -> [a]
intersparse _ []=[]
intersparse _ (lst:[])=lst
intersparse s (lst:lsts)=lst++[s]++intersparse s lsts
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容