Simple Haskell: Define Type and Simplify funcitons

定义新的数据类型

使用data关键字可以定义一个新的数据类型:

-- file: ch03/BookStore.hs
data BookInfo = Book Int String [String]
                deriving (Show)

data关键字后面的BookInfo就是新数据类型的名字,我们成BookInfo为类型构造器,首字母必须大写.

后面的Book是值构造器的名字,类型的值就是由值构造器创建的,首字母也必须大写.

Book之后的 Int String [String]是类型的组成部分,和面向对象语言的类中的域作用一致:它是一个存储值得槽.

在这个例子中,Int表示一本书的ID,String表示书名,而[String]表示作者,显然作者可以为多个.

BookInfo的成分和一个包含(Int,String,[String])的三元组一样,他们唯一不同的是类型.

可以将值构造器看做一个函数,它创建并返回某个类型值,在上面的例子中,我们将Int,String,[String]三个类型的值应用到Book,从而创建一个BookInfo类型的值:

-- file: ch03/BookStore.hs
myInfo = Book 9780135072455 "Algebra of Programming" ["Richard Bird", "Oege de Moor"]

在ghci中测试这个类型:

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

*Main> myInfo
Book 9780135072455 "Algebra of Programming" ["Richard Bird","Oege de Moor"]

在ghci中创建新的值:

*Main> Book 0 "The Book of Imaginary Beings" ["Jorge Luis Borges"]
Book 0 "The Book of Imaginary Beings" ["Jorge Luis Borges"]

在ghci中定义变量是需要使用let:

*Main> let cities = Book 173 "Use of Weapons" ["Iain M. Banks"]

类型构造器和值构造器的命名

在Haskell中,类型的名字(类型构造器)和值构造器的名字是相互独立的,类型构造器只能出现在类型的定义或者类型签名当中,而值构造器只能出现在实际的代码中,因此,将类型构造器和值构造器赋予相同的名字是没有问题的.

类型别名

使用类型别名为一个已存在的类型设置一个更具描述性的名字.

data BookReview = BookReview BookInfo CustomerID String

上面这个例子中,并没有说明最后的那个String是干什么用的,通过类型别名可以解决这个问题:

type CustomerID = Int
type ReviewBody = String

data BetterReview = BetterReview BookInfo CustomerID ReviewBody

type关键字用于设置类型别名,可以为啰嗦的类型设置一个简短的名字:

type BookRecord = (BookInfo, BookReview)

类型别名只是为类型提供了一个新名字,创建值得工作还是由原来类型的值的构造器进行.

代数数据类型

Bool是代数数据类型最简单也最常见的例子,一个代数类型可以有多于一个值构造器:

data Bool = False | True

上面代码定义的Bool类型用于两个值构造器,一个是True,一个是False.每个值构造器使用 | 符号分割,可以说成是:Bool类型由True值或False值构成.

当一个类型拥有一个以上的构造器时,这些值构造器通常称为分支或备选,同一类型的所有备选,创建出的值得类型都是相同的.

代数数据类型的各个值构造器都可以接受任意个数的参数:

type CardHolder = String
type CardNumber = String
type Address = [String]
data BillingInfo = CreditCard CardNumber CardHolder Address
                 | CashOnDelivery
                 | Invoice CustomerID
                   deriving (Show)

这个程序提供了三种付款方式.如果使用信用卡支持,就需要使用CreditCard作为值构造器,并输入信用卡卡号,持有人和地址作为参数.如果使用现金支付就不需要任何参数.如果选择货到付款方式,只需要提供客户ID就行了.

当使用值构造器类创建BillingInfo类型的值时,必须提供这个值构造器所需的参数:

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

*Main> :type CreditCard
CreditCard :: CardNumber -> CardHolder -> Address -> BillingInfo

*Main> CreditCard "2901650221064486" "Thomas Gradgrind" ["Dickens", "England"]
CreditCard "2901650221064486" "Thomas Gradgrind" ["Dickens","England"]

*Main> :type it
it :: BillingInfo

如何合适的选择元组或代数数据类型

代数数据类型使得我们可以在结构相同但类型不同的数据之间做区分,然而对于元组来说,只要元素的结构和类型一致,那么元组的类型就是相同的.

因此,如果程序使用大量的复合数据,使用data进行类型的自定义对于类型安全和可读性都有好处,小程序的话可以直接使用元组.

其他语言中类似代数数据类型的东西

结构

当只有一个值构造器时,代数数据类型和元组很相似,他将一系列相关的值打包成一个复合值.这种做法相当于C++/C中的struct,而代数数据类型的成分相当于struct中的域.

struct book_info {
    int id;
    char *name;
    char **authors;
};

C结构和Haskell代数数据类型的最大差别是,代数数据类型的成分是匿名且按位置排序的:

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

按位置排序指的是,对成分的访问时按位置进行的.而不是struct那样通过名字,book_info -> name.

枚举

C/C++中的enum用于表示一系列符号值排列.代数数据类型里面也有类似的,称为枚举类型:

enum roygbiv {
    red,
    orange,
    yellow,
    green,
    blue,
    indigo,
    violet,
};

用Haskell表示:

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

联合

如果一个代数数据类型有多个备选,可以把他看做是C/C++中的union.

两者的区别是,union并不告诉用户当前使用的是哪个备选,用户必须自己记录这方面的信息,这意味着,如果搞错了备选信息,那么对union的使用就会出错.

Haskell版本的union:

type Vector = (Double, Double)

data Shape = Circle Vector Double
           | Poly [Vector]
             deriving (Show)

模式匹配

对于某个类型的值来说,应该可以做到以下两点:

  1. 如果这个类型有多个值构造器,那么应该可以知道,这个值是由那个构造器创建的
  2. 如果一个值构造器包含不同的成分,那么应该有办法提取这些成分

模式匹配允许我们查看值得内部,并将值包含的数据绑定到变量上:

myNot True = False
myNot False = True

Haskell允许函数定义一系列等式,myNot的两个等式分别定义了函数对于输入参数在不同模式下的行为.对于每个等式,模式定义放在函数名之后,等号之前.

为了理解模式匹配是如何工作的,来研究一下myNot False是如何工作的: 首先调用myNot,运行时检查输入参数False是否和第一个模式的值构造器匹配,答案是不匹配,于是它继续尝试匹配第二个模式,这次匹配成功了,于是第二个等式右边的值作为结果返回.

这个例子计算列表所有元素之和:

sumList (x:xs) = x + sumList xs
sumList []  = 0

需要说明的是,在Haskell中,列表 [1, 2] 实际上只是 (1:(2:[])) 的一种简单的表示方式,其中(:)用于构造列表:

Prelude> []
[]

Prelude> 1:[]
[1]

Prelude> 1:2:[]
[1,2]

当需要对一个列表匹配时,也可以使用(:)操作符,只不过不是用来构造列表,而是分解列表.

sumList [1,2]时的过程: [1,2]首先会对第一个模式(x:xs)进行匹配,结果是模式匹配成功,并将x绑定为1,xs绑定为[2].

计算进行到这一步,表达式变成了 1 + (sumList [2]),于是递归调用sumList,对[2]进行匹配.

这一次也是也是在第一个模式匹配成功,变量x被绑定为2,xs被绑定为[].表达式变成了 1 + (sumList []).

再次递归调用sumList,输入为[],这一次,第二个等式的[]匹配成功,返回0,整个表达式为1 + (2 + (0)),计算结果为3.

组成和解构

更进一步

通配符模式匹配

如果在模式匹配中不在乎某个值的类型,可以使用下划线字符”_”进行标识,也叫作通配符:

nicerID      (Book id _     _      ) = id
nicerTitle   (Book _  title _      ) = title
nicerAuthors (Book _  _     authors) = authors

通配符的另一个好处是,如果我们在模式匹配中引入了一个变量,但没有在函数体中用到的话,编译器会发出一个警告.

穷举匹配模式和通配符

再给一个类型写一组匹配模式时,很重要的一点就是一定要涵盖构造器的所有可能情况.比如要匹配一个列表,就应该写一个匹配非空(:)方程和空([])方程.

记录语法

给一个数据类型的每个成分写访问函数是令人感觉重复而乏味的:

nicerID      (Book id _     _      ) = id
nicerTitle   (Book _  title _      ) = title
nicerAuthors (Book _  _     authors) = authors

我们在定义一种数据类型的同时,就可以定义好每个成分的访问器:

data Customer = Customer {
      customerID      :: CustomerID
    , customerName    :: String
    , customerAddress :: Address
    } deriving (Show)

以上代码和下面这段我们熟悉的代码意义一致:

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

我们仍然可以如往常一样使用应用语法来新建一个此类型的值:

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"
            }

参数化类型

曾经提到列表类型是多态的:列表中的元素可以是任何类型.同样可以给自定义的类型添加多态性,只要在类型变量中使用类型变量就可以.

Prelude中定义了一种叫做maybe的类型:它用来表示既可以有值,也可以空缺.比如数据库中的某字段可以为空.

data Maybe a = Just a
             | Nothing
译注:Maybe,Just,Nothing 都是 Prelude 中已经定义好的类型
这段代码是不能在 ghci 里面执行的,它简单地展示了标准库是怎么定义 Maybe 这种类型的

上面的变量a不是普通的变量,它是一个类型变量.它一位置`Maybe类型使用另一种类型作为他的参数,从而使得maybe可以作为任何类型的值.

someBool = Just True
someString = Just "something"

maybe是一个多态或者称作泛型的类型.我们想maybe类型构造器传入某种类型作为参数, 例如 `Maybe Int,或者 `Maybe [Bool].

参数化类型大致相当于C++中的Template或者Java中的泛型.

递归类型

列表这种常见的类型就是递归的:即用它自己定义自己.我们定义一个与列表类似的类型,用 Cons 替换 (:) 构造器,用Nil替换[]构造器:

data List a = Cons a (List a)
            | Nil
              deriving (Show)

List在等号的左右两侧都出现,我们可以说该类型的定义引用了它自己.当我们用 Cons构造器创建一个值时,必须提供一个a作为参数一,以及一个List a类型的参数作为参数二.

我们能创建的List a的最简单的值就是Nil:

ghci> Nil
Nil

由于Nil是一个List a类型,因此我们可以将它作为Cons的第二个参数.

ghci> Cons 0 Nil
Cons 0 Nil

然后 Cons 0 Nil 也是一个 List a 类型,同样可以作为Cons的第二个参数:

ghci> Cons 1 it
Cons 1 (Cons 0 Nil)
ghci> Cons 2 it
Cons 2 (Cons 1 (Cons 0 Nil))
ghci> Cons 3 it
Cons 3 (Cons 2 (Cons 1 (Cons 0 Nil)))

可以得到一个很长的 Cons 链,其中每个子链的末位元素都是一个 Nil.