第三章:类型定义、流和函数

定义新类型 p41-43

定义一个类型需要几个部分:

  1. 类型名(type name, type constructor)
  2. 值构造器(value constructor, data constructor)
  3. 组成元素(components)

举一个例子,一个书籍类型的定义可能是这样的:

-- file: chp3/BookStore.hs

data BookInfo = Book Int String [String]
                deriving (Show)

其中data关键字之后的BookInfo是类型名,Book是值构造器,而之后的语句则是组成元素。

你可能会奇怪为什么类型名的作用和值构造器的作用似乎类似,其实是不一样的,类型名用于类型的定义,而值构造器负责生成该类型的值。

如果你熟悉面向对象编程的话,你可以将类型名比作类名(比如一个Python中的类Queue),而将值构造器看作对象生成器(比如Python中的__init__)。

Prelude> :load BookStore
[1 of 1] Compiling Main             ( BookStore.hs, interpreted )
Ok, modules loaded: Main.

*Main> :type Book   -- 查看值构造器所使用的参数
Book :: Int -> String -> [String] -> BookInfo

*Main> let sicp = Book 123123 "SICP" ["ha", "gjs"]  -- 用值构造器(Book)生成值
*Main> sicp
Book 123123 "SICP" ["ha","gjs"]

*Main> :type sicp   -- 值的类型是BookInfo
sicp :: BookInfo

*Main> :info BookInfo   -- 用:info查看BookInfo类型的详细信息
data BookInfo = Book Int String [String]
    -- Defined at BookStore.hs:3:6-13
instance Show BookInfo -- Defined at BookStore.hs:4:27-30

那么,类型名和值构造器是否可以使用相同的名字? 答案是可以,而且这种用法非常常见。

-- file: chp3/AnotherBookStore.hs

data Book = Book Int String [String]
            deriving (Show)

运行:

Prelude> :load AnotherBookStore
[1 of 1] Compiling Main             ( AnotherBookStore.hs, interpreted )
Ok, modules loaded: Main.

*Main> let tij = Book 245 "Thinking in Java" ["Bruce Eckel"]
*Main> tij
Book 245 "Thinking in Java" ["Bruce Eckel"]

*Main> :type tij
tij :: Book

*Main> :info Book
data Book = Book Int String [String]
    -- Defined at AnotherBookStore.hs:3:6-9
instance Show Book -- Defined at AnotherBookStore.hs:4:23-26

别名 p43-44

我们可以用type关键字给一个已有的类型一个别名(synonyms),主要为了增强程序的可读性。

比如我们可以用特定的名字替换之前的BookInfo类型的组成元素的名字。

-- file: chp3/BookStore_version_2.hs

type Id = Int
type Title = String
type Authors = [String]

data BookInfo = Book Id Title Authors
                deriving (Show)

运行:

*Main> :load BookStore_version_2
[1 of 1] Compiling Main             ( BookStore_version_2.hs, interpreted )
Ok, modules loaded: Main.

*Main> :info BookInfo
data BookInfo = Book Id Title Authors
    -- Defined at BookStore_version_2.hs:7:6-13
instance Show BookInfo -- Defined at BookStore_version_2.hs:8:27-30

代数数据类型 p44-49

代数数据类型(Algebraic Data Type)就是可以拥有多个值构造器的类型。

比如下面的Roygbiv类型就是一个代数数据类型:

-- file: chp3/Roigbiv.hs

data Roygbiv = Red
             | Orange
             | Yellow
             | Green 
             | Blue
             | Indigo
             | Violet
               deriving (Eq, Show)

模式匹配 p50-54

Haskell允许我们将函数组织成一系列方程(equation)等式,然后使用该函数的时候,对比每个方程直到找到匹配(相等)的方程,然后执行方程内的表达式,这一机制称之为模式匹配(pattern match)

比如要写一个我们自己的not操作符,可以这样写:

-- file: chp3/myNot.hs

myNot True = False
myNot False = True

假如我们执行myNot False的话,Haskell就会对比第一个方程,然后发现不匹配,于是对比第二个方程,然后匹配成功,于是执行第二个方程内的表达式,也即是,返回True

还有一个更直观的例子,我们可以写一个函数checkIt比对输入数值,对01输出good,其他数值输出bad

不过问题是整数值的范围很大,我们不可能一个个地写匹配:

checkIt 0 = "good"
checkIt 1 = "good"
checkIt 2 = "bad"
checkIt 3 = "bad"
checkIt 4 = "bad"
-- 很多很多。。。

这时你需要一个Wild Card Pattrn来搭救你,它可以作为模式匹配的通用匹配(相当于else),所有和Wild Card Pattrn对比的模式都会匹配。

Wild Card Pattrn需要你使用一个_作为匹配符,使用它,我们上面的checkIt函数可以这样写:

-- file: chp3/checkIt.hs

checkIt 0 = "good"
checkIt 1 = "good"
checkIt _ = "bad"  -- wild card pattern

运行:

*Main> :load checkIt
[1 of 1] Compiling Main             ( checkIt.hs, interpreted )
Ok, modules loaded: Main.

*Main> checkIt 0
"good"

*Main> checkIt 3
"bad"

结构 p51-p4

之前我们说可以构造一个新类型,比如:

data BookInfo = Book Int String [String]
                deriving (Show)

创建一个新数据项可以调用类似语句Book ... ... ...,用值构造器Book组合各个成分,生成新的值,称之为构造(construction)

反过来想,我们也希望可以有一种方法,可以重新获取值里面的各个成分。

用模式匹配可以做到这一点:

bookId (Book id title authors) = id
bookTitle (Book id title authors) = title
bookAuthors (Book id title authors) = authors

举个例子,当我们执行bookId (Book 123 "a book" ["author_A, "author_B"],模式匹配将各个成分一一比对:将123放入id项,将a book放入title,将["author_A", "author_B"]放入authors,然后抽取出bookId所需要的部分——id,并将其返回,于是我们就取得了值的id成分,这一过程称之为分解(deconstruction)

完整代码:

-- file: chp3/deconstruction.hs

type Id = Int
type Title = String
type Authors = [String]

data Book = Book Id Title Authors 
            deriving (Show)

bookId (Book id title authors) = id
bookTitle (Book id title authors) = title
bookAuthors (Book id title authors) = authors

运行:

Prelude> :load deconstruction
[1 of 1] Compiling Main             ( deconstruction.hs, interpreted )
Ok, modules loaded: Main.

*Main> let clrs = Book 1111 "Introduction to Algorithms" ["c", "l", "r", "s"]

*Main> clrs
Book 1111 "Introduction to Algorithms" ["c","l","r","s"]

*Main> bookId clrs
1111

*Main> bookTitle clrs
"Introduction to Algorithms"

*Main> bookAuthors clrs
["c","l","r","s"]

如果我们只想抽取出其中一样成分,而对其他成分没有兴趣的话,我们还可以用Wild Card Pattrn

-- file: chp3/deconstruction.hs

type Id = Int
type Title = String
type Authors = [String]

data Book = Book Id Title Authors 
            deriving (Show)

bookId (Book id _ _) = id
bookTitle (Book _ title _) = title
bookAuthors (Book _ _ authors) = authors

Note

如果你熟悉面向对象的话,那把构造想成是新建一个对象实例,而分解则是对象的访问方式(getter)。

Record Syntax p55-56

上面说明了可以用模式匹配获取值的成分,但这一方法有点麻烦,因为它要求各个访问方法和值的构造一一对应——而值的成分越复杂,我们需要写的额外代码就越多:

-- file: chp3/deconstruction.hs

type Id = Int
type Title = String
type Authors = [String]

data Book = Book Id Title Authors 
            deriving (Show)

bookId (Book id _ _) = id
bookTitle (Book _ title _) = title
bookAuthors (Book _ _ authors) = authors

其实我们还有一种更好的办法,可以在定义类型的时候连访问方法一并定义:

-- file: chp3/record_syntax.hs

type Id = Int
type Title = String
type Authors = [String]

data Book = Book {
                bookId :: Id,
                bookTitle :: Title,
                bookAuthors :: Authors 
            } deriving (Show)

上面的两种方法效果是几乎一样,只有点小不同:

Prelude> :load record_syntax
[1 of 1] Compiling Main             ( record_syntax.hs, interpreted )
Ok, modules loaded: Main.

*Main> let note = Book 10086 "huangz's note" ["huangz"]

-- 输出和之前的方式有点不同
*Main> note
Book {bookId = 10086, bookTitle = "huangz's note", bookAuthors = ["huangz"]}

*Main> bookId note
10086

*Main> bookTitle note
"huangz's note"

-- 另外,构造值的时候,可以通过指定成分名字而打乱成分的顺序
*Main> let hp = Book { bookId = 2552, bookAuthors = ["j.k loli"], bookTitle = "mary poter" }

*Main> hp
Book {bookId = 2552, bookTitle = "mary poter", bookAuthors = ["j.k loli"]}

Note

注意当按顺序创建值时,不需要用{}包围参数。

多态类型 p57-58

之前我们看过了一些多态的函数,比如head:

*Main> :t head
head :: [a] -> a

这里的a就是类型变量。

实际上,我们可以通过在定义类型时加入类型变量,让所定义的类型支持多态,比如下面的一个二叉树:

-- file: chp3/tree.hs

data Tree a = Node a (Tree a) (Tree a)
            | Empty
              deriving (Show)

其中a为类型变量(字母a只是一个习惯,使用其他字母代替也是可以的)。

运行:

Prelude> :load tree
[1 of 1] Compiling Main             ( tree.hs, interpreted )
Ok, modules loaded: Main.

-- 一棵整数值树
*Main> let num_tree = Node 10 Empty Empty

*Main> num_tree
Node 10 Empty Empty

*Main> :type num_tree
num_tree :: Tree Integer

-- 一棵单字符树
*Main> let c_tree = Node 'f' (Node 'a' Empty Empty) (Node 'z' Empty Empty)

*Main> c_tree
Node 'f' (Node 'a' Empty Empty) (Node 'z' Empty Empty)

*Main> :type c_tree
c_tree :: Tree Char

注意当我们执行:type num_tree:type c_tree时,返回的值各有不同。

错误报告 p60-61

抛出错误可以使用error方法,它中断编译并返回错误信息:

-- file: chp3/error.hs

guess :: Int -> String

guess num = if num == 10086
            then "luckly"
            else error "bad~ bad~ bad~"

guess函数只有在收到10086的时候才不会出错。

*Main> guess 1
"*** Exception: bad~ bad~ bad~

*Main> guess 50
"*** Exception: bad~ bad~ bad~

*Main> guess 10086
"luckly"

局部变量 p61-63

let p.61

let结构可以让你建立局部变量,格式如下:

let ...
in ...

以下是一个关于借钱的lend函数:

-- file: chp3/lending.hs

lend amount balance = let reverse = 100
                          newBalance = balance - amount
                      in if balance < reverse
                         then Nothing
                         else Just newBalance

遮蔽 p.62

let可以互相嵌套,如果内层和外层有同样的名字时,内层的名字覆盖外层的名字,这一现象称之为遮蔽(shadowing)

例如在下面的表达式中,将打印出("foo", 1)

let x = 1
    in ((let x = "foo" in x), x)

where p.63

where结构和let类似,都可以定义局部变量,但是where将局部变量的赋值放到表达式后面进行,而let放在前面。

下面的lend函数和上面用let定义的作用一样:

-- file: chp3/where.hs

lend amount balance = if amount < reverse * 0.5
                      then Just newBalance
                      else Nothing
                      where reverse = 100
                            newBalance = balance - amount

局部函数 p63-64

letdouble不但可以用来定义变量,还可以用来定义函数(其实它们都是表达式):

Prelude> let double num = num * 2 in double 5
10

case结构 p.66-67

case结构格式:

case value of
    pattern_1 -> expression_1
    pattern_2 -> expression_2
    ...
    pattern_n -> expression_n
    _ -> default_expression

下面是一个识别问候语,并返回相应问候语的程序:

-- file: chp3/case.hs

greet :: String -> String
greet value = case value of
                "hello" -> "world"
                "good morninig" -> "morninig"
                _ -> "hello"

运行:

*Main> :load case
[1 of 1] Compiling Main             ( case.hs, interpreted )
Ok, modules loaded: Main.

*Main> greet "hello"
"world"

*Main> greet "long time no seeeeeeeeeeee~"
"hello"

守卫 p.68-70

Haskell还提供了另一种和模式匹配case以及if很相似、但结构更清晰的匹配方法,称之为守卫(guard)

我们用守卫重写上面的问候语程序:

-- file: chp3/guard.hs

greet :: String -> String

greet value
    | value == "hello"        = "hello"
    | value == "good morning" = "morning"
    | otherwise               = "hello"

可以看到,守卫的每个匹配由|分割,每个匹配需是一个返回布尔值的表达式(比如value == "hello",而结果表达式则放到等号之后(比如world)。

if、case、守卫以及模式匹配

if结构、case结构、守卫以及模式匹配的作用都非常相似,值得我们仔细地研究一下它们的微小区别,以及它们适用的地方。

if

首先,if的结构最简单:

if predicate
then expression_1
else expression_2

如果predicateTrue,则执行expression_1;如果为False,则执行expression_2

很明显,if结构适用于只有两种情形的匹配,一旦情况超过两种,if语句就需要嵌套使用,可读性就会大大降低。

比如下面一个例子,用if写起来就非常麻烦:

check value = if value == 10
              then "value == 10"
              else if value == 3
              then "value == 3"
              else if value == 5
              then "value == 5"
              else "value not equals to 10, 3 or 5"

case

case语句的结构如下:

case value of
    value_1 -> expression_1
    value_2 -> expression_2
    ...
    value_n -> expression_n
    _ -> default_expression

case结构将value和结构体内的各个值对比(value_1value_2。。。),当某一个值对比成功时,则执行相应的表达式。

如果所有对比都不成功,就执行(可能有的)Wild Card Pattrn。

可以看出,case最适合的是值之间对比的情况。

比如说,用case来写上面check函数最适合不过了:

check value = case value of
                10 -> "value == 10"
                3  -> "value == 3"
                5  -> "value == 5"
                _  -> "value not equals to 10, 3 or 5"

case的缺点(也是它的优点)是不能在对比式中写表达式,所以它做不到像if语句那样的事情:

if value * 5
then ...
else ...

当然这个缺点可以用let或者where来克服,只是这样一来就不如case或守卫方便了:

let new_value = value * 5
in case new_value of
    ... -> ...

守卫

守卫的格式是用|分割每条匹配:

| predicate_1 = expression_1
| predicate_2 = expression_2
| ...
| otherwise

守卫按顺序地检查每一条判断语句,找到合适的就执行相应的表达式。

守卫可以看作是if的多匹配版本,它和if一样可以在判断语句内执行表达式计算。

check语句的守卫版本:

check value
    | value == 5  = "value == 5"
    | value == 10 = "value == 10"
    | value == 3  = "value == 3"
    | otherwise   = "value not equals to 5, 10 or 3"

模式匹配

模式匹配和上面的三种对比结构不尽相同,它是以函数定义的方法来匹配对比式,最大的好处是模式匹配写的语句非常直白和易读。

用模式匹配写的check函数可能是四种匹配方法中最易读的。

check 10 = "value == 10"
check 5  = "value == 5"
check 3  = "value == 3"
check _  = "value not equals to 10, 5 or 3"

而另一方面,模式匹配的功能不如前面的三种语句,比如你没有办法在模式匹配里比对范围,也不能在对比式中写表达式。

想想如果我们的check函数不是比对三个值(3510),而是一个范围形函数,用模式匹配是没有办法写完这种匹配的(除非你是机器人。。。):

check 1 = "bad"
check 2 = "bad"
...
check 10086 = "good"    -- 只有10086是"好"数
check 10087 = "bad"
check 10089 = "bad"
...

小结

如果匹配只有两种情况,使用if结构最方便。

如果匹配情况很多,而且需要在对比式中写表达式(比如value * 2 > 10086)的话,你应该使用守卫。

如果匹配情况很多,但全都是简单的对比(value == 10086这样的语句的话,你应该使用case

模式匹配可以用于定义匹配情况比较少的函数,但它不适合情况较多或具有范围性的匹配。