《Real World Haskell》读书笔记

这个是我个人关于《Real World Haskell》的读书笔记,希望对你也有用。

我会随着阅读的进度更新这个笔记。

目录:

第一章:起步

算数

Haskell中的算数默认使用中序格式(infix form),也可以将操作符用括号包围,然后使用前序格式(prefix form)

Prelude> 2 + 2
4

Prelude> (+) 2 2
4

注释

Haskell使用--作为注释符号。

-- 这是一行注释

负数

当负号操作符-有两个操作符是,Haskell实现会读出歧义,所以必须用括号包围。

比如当执行f -2时,Haskell不知道你的意思是变量f减去常量2还是执行函数f的调用,参数为-2

Prelude> -2
-2

Prelude> 2 + -2

<interactive>:1:1:
    Precedence parsing error
    cannot mix `+' [infixl 6] and prefix `-' [infixl 6] in the same infix expression

Prelude> 2 + (-2)
0

逻辑操作符于值比对

Haskell使用True表示真,False表示假(大小写必须正确)。

逻辑操作符:&&表示并,||表示或,not表示反。

Prelude> True && True
True

Prelude> False || True
True

Prelude> not True
False

和其他很多语言不同的是,Haskell中以非零或空字符表示假,也不用非空字符和大于0的数值表示真。

Prelude> True && 1

<interactive>:1:9:
    No instance for (Num Bool)
    arising from the literal `1'
    Possible fix: add an instance declaration for (Num Bool)
    In the second argument of `(&&)', namely `1'
    In the expression: True && 1
    In an equation for `it': it = True && 1

Haskell的对比符和其他语言相似,像是==>=,唯一比较特立独行的是不等符号,Haskell使用/=表示不相等。

Prelude> 1 == 1
True

Prelude> 1 > 2
False

Prelude> 3 <= 2.5
False

Prelude> 1 /= 1
False

操作符优先级

和大多数语言一样,Haskell使用括号来包裹需要优先计算的表达式。

默认的操作符也遵守一般的数学符号优先级(比如*优先+,等等),一共分为1至9个优先级,优先级高的操作符先计算。

下面的例子表明+操作符优先级为6,而*操作符优先级为7

Prelude> :info (+)
class (Eq a, Show a) => Num a where
  (+) :: a -> a -> a
  ...
    -- Defined in GHC.Num
infixl 6 +

Prelude> :info (*)
class (Eq a, Show a) => Num a where
  ...
  (*) :: a -> a -> a
  ...
    -- Defined in GHC.Num
infixl 7 *

绑定变量、未绑定变量及预定义变量

使用未绑定变量会出错。

Prelude> luckly_number

<interactive>:1:1: Not in scope: `luckly_number'

绑定变量使用let语句。

Prelude> let luckly_number = 10086

Prelude> luckly_number
10086

在库里面,有时也定义了一些预定义变量,比如Prelude里的pi

Prelude> pi
3.141592653589793

列表

用方括号包裹一簇元素的类型称之为列表(list),列表可以是空的或者是非空的,但必须拥有相同类型的值

Prelude> let one_two_three = [1, 2, 3]

Prelude> let empty_list = []

Prelude> [1, 2, "error"]

<interactive>:1:5:
No instance for (Num [Char])
    arising from the literal `2'
Possible fix: add an instance declaration for (Num [Char])
In the expression: 2
In the expression: [1, 2, "error"]
In an equation for `it': it = [1, 2, "error"]

列表的一个有用功能可以指定迭代的起始值、结束值和步长,让列表自动生成值,称之为enumeration

-- 生成1至10内所有数值
Prelude> [1..10]
[1,2,3,4,5,6,7,8,9,10]

-- 生成1至20内所有奇数
Prelude> [1, 3 .. 20]
[1,3,5,7,9,11,13,15,17,19]

-- 生成10至1内所有数值
Prelude> [10, 9 .. 1]
[10,9,8,7,6,5,4,3,2,1]

Warning

使用浮点数进行enumeration要小心,比如语句[1.0 .. 1.8]将返回[1.0, 2.0],因为Haskell对1.8进行了舍入操作。

拼接列表使用++操作符:

Prelude> [1, 2] ++ [3, 4]
[1,2,3,4]

将单个元素加入到列表使用:操作符:

Warning

:操作符的第二个操作对象必须是列表,调用诸如[1, 2] : 3这样的语句将抛出错误。

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

字符串和单个字符

Haskell使用双引号"包裹字符串(text string),用单引号'包裹单字符(character)

Prelude> "hello world"
"hello world"

Prelude> 'c'
'c'

实际上,字符串也是一个列表,里面每个元素都是一个单字符:

Prelude> let greet = ['h', 'e', 'l', 'l', 'o']

Prelude> greet
"hello"

所以列表上的各种操作,字符串也可以使用:

Prelude> greet ++ " world"
"hello world"

Prelude> 'h' : "ello"
"hello"

函数使用

在没有歧义的情况下,一般不必使用括号包裹函数参数。

Prelude> odd 3
True

Prelude> compare 2 3
LT

函数调用的优先级比操作符高,所以一般也不用使用括号包围,比如下列语句是相等的:

Prelude> compare 2 3 == LT
True

Prelude> (compare 2 3) == LT
True

Haskell的函数是左结合的,在一些可能产生歧义的语句,括号是必须的:

Prelude> compare (sqrt 3) (sqrt 4)
LT

Prelude> compare sqrt 3 sqrt 4

<interactive>:1:1:
    The function `compare' is applied to four arguments,
    but its type `a0 -> a0 -> Ordering' has only two
    In the expression: compare sqrt 3 sqrt 4
    In an equation for `it': it = compare sqrt 3 sqrt 4

Note

函数调用是左结合的,也即是对语句a b c d,执行(((a b) c) d)

函数类型

当一个相同的参数传入一个给定函数,总能返回固定结果的函数,该函数称之为纯(pure)的。

而对一些带有副作用(side effect)、输入不明确、或需要调用外部资源的函数,我们称之为不纯(impure)的。

操作符也是函数

在Haskell中,操作符也是函数,比如||+,等等。

其他

Haskell的类型名必须以大写字母开头(比如IntString),而变量名则必须以小写字母开头:

Prelude> let BadVariableName = 1

<interactive>:1:5: Not in scope: data constructor `BadVariableName'

:type语句可以查看变量的类型,GHC中的快捷方式为:t

Prelude> :type 1
1 :: Num a => a

:module +载入模块,在GHC中也可以用快捷方式:m +

Prelude> :module +Data.Ratio
Prelude Data.Ratio>

GHC使用一个特殊变量it储存最后一个表达式的值。

Prelude Data.Ratio> 1 + 1
2

Prelude Data.Ratio> it
2

第二章:类型和函数

什么是类型 p17

在Haskell中,所有的表达式和函数都有类型。

类型系统为我们提供抽象,并隐藏底层细节。

Haskell中的类型 p17-21

在Haskell中,类型是强类型(strong)静态(static)和可以被自动推导出的(automaticllly inferred)。

强类型

一个语言的类型为强类型,表示该语言的类型系统不会容忍任何类型错误。

比如用一个字符串和一个数字相加,给原本接受列表的函数传入数字,这些都是类型错误,而Haskell不会允许有这类错误的语句运行。

另一个强类型的特点是,它不会对值进行任何的自动转型(Coercion, conversion, casting)

比如在很多语言中1 && True返回一个True,因为1会被自动转型为True,然后语句变成True && True。但是在Haskell中,Haskell不会将1自动转型成True,它只会报告说两个不同的类型试图进行逻辑比较,这是一个错误的表达式。

如果你需要类型转换,那你必须手动显式进行。

静态类型

一个语言是静态类型的表示它的所有表达式的类型必须在编译的时候被知道,Haskell也一样,这也即是是说,Haskell编译时如果发现类型错误,就会中止编译并报错,当一个Haskell程序被成功编译时,我们可以确信该程序没有类型错误。

类型推导(type inference)

Haskell编译器在绝大部分时间内可以自动推导出表达式的类型,我们也可以显式地指定每个表达式的类型,但这通常不是必须的。

Haskell的一些常用基本类型 p21

Char

单字符,表示为Unicode。

Bool

布尔变量,包括TrueFalse

Int

原生整数类型,最大值由机器决定(通常是32bit或64bit)。

Integer

不限长度的整数类型。

Double

浮点数。

类型签名(type signature) p22

一般来说,Haskell可以推导出表达式的类型,但是,我们也可以用类型签名显式地指定类型。 类型签名的格式是expression :: type

查看一个变量或函数的类型签名可以使用 :type语句。

Prelude> 'a' :: Char
'a'

Prelude> 1 :: Int
1

Prelude> :type 1    -- 自动推导
1 :: Num a => a

复合数据类型:列表(list)和元组(tuple) p23

复合数据类型既是组合使用其他类型的类型。

Haskell中最常见的复合数据类型是列表和元组。

列表和元组都可以组合数据,但它们也有一些不同,两种类型的对比如下:

类型 可组合类型 长度 可用函数
列表 只能组合相同类型 长度可变 ++, :, head, ...
元组 可以组合相同或不同类型 固定长度 fst, snd

列表 p23

列表只能组合相同类型的数据,它是长度可变的,可以利用++等函数进行伸展或收缩,还有一大类其他常用函数可以对列表进行操作。

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

Prelude> [1, 2] ++ [3]
[1,2,3]

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

Prelude> [1 ,2] ++ "hello"  -- 列表不能组合不同类型

<interactive>:1:5:
    No instance for (Num Char)
      arising from the literal `2'
    Possible fix: add an instance declaration for (Num Char)
    In the expression: 2
    In the first argument of `(++)', namely `[1, 2]'
    In the expression: [1, 2] ++ "hello"

元组 p24

元组(tuple)可以组合不同类型的数据,它是定长的(长度不变),所以也没有像列表那样的对元组进行伸缩处理的函数。

因为元组的以上性质,所以它们通常只单纯用于保存数据,如果需要处理数据,一般使用列表。

Prelude> (1, "hello", 'c')  -- 储存不同类型数据
(1,"hello",'c')

Prelude> (1, 2, 3)  -- 也可以储存相同类型的数据
(1,2,3)

Prelude> (1, 2, 3) ++ (4)   -- 不可以用列表的处理函数

<interactive>:1:1:
    Couldn't match expected type `[a0]' with actual type `(t0, t1, t2)'
    In the first argument of `(++)', namely `(1, 2, 3)'
    In the expression: (1, 2, 3) ++ (4)
    In an equation for `it': it = (1, 2, 3) ++ (4)

通常用n-tuple表示不同长度的元组,比如1-tuple表示只有一个元素的元组,而2-tuple表示有两个元素的元组,以此类推。。。

在Haskell中,没有1-tuple,假如你输入(1),那你至获得一个数字值1

Prelude> (1)
1

Prelude> :type it
it :: Integer

Prelude> (1, 2)
(1,2)

Prelude> :type it
it :: (Integer, Integer)

2-tuple比较特殊,作用在它们之上有两个函数:fstsnd,它们分别获取元组的头元素和第二元素。

Warning

如果你熟悉Lisp,注意这里的fstsnd函数和Lisp里面的carcdr是不同的,Lisp里的carcdr可以作用于任何长度的列表,而Haskell里的fstsnd只能作用于2-tuple。

Prelude> let greet = ("hello", "huangz")

Prelude> fst greet
"hello"

Prelude> snd greet
"huangz"

Prelude> fst (1, 2, "morning")  -- fst和snd只能对2-tuple使用

<interactive>:1:5:
    Couldn't match expected type `(a0, b0)'
    with actual type `(t0, t1, t2)'
    In the first argument of `fst', namely `(1, 2, "morning")'
    In the expression: fst (1, 2, "morning")
    In an equation for `it': it = fst (1, 2, "morning")

另一方面,如果你熟悉Python,你可能想当然地认为元组的函数和列表的函数是通用的,就像Python里的列表和元组一样。

而实际上,Haskell里的列表和元组的函数不是通用的。

Prelude> head [1, 2, 3] -- head获取列表头元素
1

Prelude> head (1, 2, 3)

<interactive>:1:6:
    Couldn't match expected type `[a0]' with actual type `(t0, t1, t2)'
    In the first argument of `head', namely `(1, 2, 3)'
    In the expression: head (1, 2, 3)
    In an equation for `it': it = head (1, 2, 3)

多态 p23-25, p36-38

其实对于列表(还有Haskell里面的其他东西)来说,还有一个很有用的地方我们已经使用了但是没有注意到,就是函数里面的多态(polymorphic)

比如对于一个列表来说,无论它里面储存的是什么类型的值,我们都可以用head取出它的数据:

Prelude> head [1..10]   -- 数值列表
1

Prelude> head ["good", "morning"]   -- 字符串列表
"good"

Prelude> head "sphinx"  -- 字符串(单字符列表)
's'

对各种类型的列表,head函数都可以返回正确的值。

我们可以试试用:type打开head的类型签名,看看里面有什么:

Prelude> :type head
head :: [a] -> a

在看看++操作符(它要用括号包裹起来):

Prelude> :type (++)
(++) :: [a] -> [a] -> [a]

我们发现两个函数里面的签名都有a,但是如果我们查看一个字符串(String)类型专用的函数words,则有不一样的发现:

Prelude> :type words
words :: String -> [String]

和列表不一样的是,words的函数签名里没有a,只有String

head函数、++函数和words函数有什么不同?

答案是head++是多态的,而words不是——也即是说,words只能处理字符串类型,而head++不在乎列表内储存的是什么类型,它只要求传入的参数是一个列表即可。

仔细观察head函数的定义(++也是类似的):

Prelude> :type head
head :: [a] -> a

这里a是一个类型变量(type variable),它可以是任何类型,就像数学里的代数一样:给它一个字符串类型的列表,它就可以处理String类型,给它一个整数值类型的列表,它就可以处理Int类型,诸如此类,这一方式称之为参数多态(parametric polymorphism)

整条类型签名的意思就是:head函数接受一个a类型的列表([a]),然后返回一个a,其中返回值的类型和之前列表里面保存的元素的类型一致,但是它不要求a是什么类型,它不在乎。

另一方面,看看words函数:

Prelude> :type words
words :: String -> [String]

这里它的签名意思是:words接受一个String类型值,然后返回一个String类型的列表([String])。

这个String是一个类型名,而不是一个类型变量,它指定了words函数只能接受String类型的值,所以它不是多态的。

Note

这也说明了,为什么类型名只能以大写字母开头,因为它必须和类型变量区别开来。

编写简单函数,并载入它 p27

我们可以编写一个函数,然后载入到GHC当中使用:

-- file: chp2/add.hs

add a b = a + b

Note

在函数定义中,我们并没有像很多语言那样使用return返回函数的值,因为Haskell中,函数是一簇表达式(expression),而不是一条条语句(statement),表达式的值就是函数的值。

然后用:load载入:

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

*Main> add 2 3
5

Note

GHC中的语句和Haskell有部分是不同的,如果你在GHCI中输入add a b = a + b,GHCI会返回一个错误。

变量 p28-29

在Haskell中(很多其他函数式编程语言也是类似),变量是不可以被重复赋值的,也即是,将一个变量名(variable name)和一个表达式(可以是一个值、一个函数或其他什么东西)绑定之后,这个变量名总是代表这个表达式,而不会指向另外一些别的东西。

我们编写一个重复定义某个变量值的程序:

-- file: source/chp2/assign.hs

luckly_number = 10086

-- 尝试重复赋值
luckly_number = 123 

然后尝试载入Haskell里运行:

Prelude> :load assign
[1 of 1] Compiling Main             ( assign.hs, interpreted )

assign.hs:6:1:
Multiple declarations of `Main.luckly_number'
Declared at: assign.hs:3:1
assign.hs:6:1
Failed, modules loaded: none.

条件求值 p29-32

Haskell中if语句的格式如下:

if -- predicate
then -- expression if predicate is True
else -- expression is predicate is False

其中thenelse之后的表达式称之为分支(branch),分支的类型必须相同,否则编译器会报错。

换个角度来说,因为Haskell中每个表达式都有一个值,而函数的值也是一个表达式,所以一个函数不应该返回不同的两种值。

Prelude> if True then 1+1 else 4
2

Prelude> if True then 1+1 else "oops~~~"

<interactive>:1:16:
    No instance for (Num [Char])
    arising from the literal `1'
    Possible fix: add an instance declaration for (Num [Char])
    In the second argument of `(+)', namely `1'
    In the expression: 1 + 1
    In the expression: if True then 1 + 1 else "oops~~~"

另一方面,当我们使用命令式语言时,通常可以省略else,因为在这些语言中else是一个语句。

但在Haskell中,因为它是一个表达式,所以我们也不能省略else表达式。

Prelude> if True then 1+1

<interactive>:1:17: parse error (possibly incorrect indentation)

我们写一个与列表函数drop一样的函数myDrop作为演示:

-- file: chp2/myDrop.hs

myDrop n xs = if n <= 0 || null xs
              then xs
              else myDrop (n-1) (tail xs)

如果你愿意,也可以将myDrop写成一行

-- file: chp2/myDropInOneLine.hs

myDrop n xs = if n <= 0 || null xs then xs else myDrop (n-1) (tail xs)

惰性求值 p32-36

通常语言有两种求值方式,一种是严格求值(strict evaluation),另一种是非严格求值(nonstrict evaluation)

Haskell默认使用非严格求值,也称惰性求值(lazy evaluation)

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

定义新类型 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

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

第四章:函数式编程

中序函数

我们通常以前序(prefix)调用函数,像head ["haha", "nono", "yoyo"],但有时使用中序(infix)会更符合阅读习惯。

比如一个对两个数进行加法的plus函数,使用1 plus 2plus 1 2更直观。

但是在Haskell中,直接写1 plus 2是不行的,Haskell只会认为1是函数方法,而plus2则是传入给它的参数,最终造成一个错误:

Prelude> let plus a b = a + b
Prelude> 1 plus 2

<interactive>:1:1:
    No instance for (Num ((a0 -> a0 -> a0) -> a1 -> t0))
        arising from the literal `1'
    Possible fix:
        add an instance declaration for
        (Num ((a0 -> a0 -> a0) -> a1 -> t0))
    In the expression: 1
    In the expression: 1 plus 2
    In an equation for `it': it = 1 plus 2

要使用中序操作符,我们需要用`包围相应的函数名,这样Haskell才能明白我们的真正意思:

Prelude> 1 `plus` 2
3

另一方面,我们不但可以用传统的前序方式定义函数,还可以用中序方法来定义。

只需要一次定义(无论前序或中序),函数都可以被用于前序调用或后续调用。

比如下面两个定义的作用是一样的:

plus a b = a + b

a `plus` b = a + b

List结构的常用函数

p77-84

List结构是很多函数式编程语言的强力工具之一,在List结构之上通常有一族功能丰富的函数,Haskell也一样。

length

length返回列表的长度:

Prelude> :type length
length :: [a] -> Int

Prelude> length [1..10]
10

Prelude> length []
0

null

null检查一个列表是否为空:

null :: [a] -> Bool

Prelude> null []
True

Prelude> null [1, 2, 3]
False

last

last返回列表的最后一个元素:

Prelude> :type last
last :: [a] -> a

Prelude> last []
*** Exception: Prelude.last: empty list

Prelude> last [1..10]
10

tail

tail返回列表除第一个元素之外的所有元素

Prelude> :type tail
tail :: [a] -> [a]

Prelude> tail []
*** Exception: Prelude.tail: empty list

Prelude> tail [1]
[]

Prelude> tail [1..10]
[2,3,4,5,6,7,8,9,10]

init

init返回列表除最后一个元素之外的所有元素:

Prelude> :type init
init :: [a] -> [a]

Prelude> init []
*** Exception: Prelude.init: empty list

Prelude> init [1]
[]

Prelude> init [1..5]
[1,2,3,4]

Note

headinit这类函数作用在空列表的时候,会抛出一个错误。

Note

当你要检查一个列表是否为空时,你可能会使用length list == 0

实际上,Haskell的List并不保存自己的长度,也即是,要获得一个List的长度,你必须遍历整个List——对长List来说,这是个相当耗时的操作。

如果执行下面代码,Haskell将一直对列表进行迭代:

length [1..]

而执行null,代码会立即返回:

null [1..]

所以当你要检查一个列表是否为空时,应该使用null而不是length

++

++函数用两个List组成一个List:

Prelude> :type (++)
(++) :: [a] -> [a] -> [a]

Prelude> "hello " ++ "moto"
"hello moto"

concat

concat将给定的一个List中的List中的元素提取出来,组合成一个List:

Prelude> :type concat
concat :: [[a]] -> [a]

Prelude> concat ["hallo", " moto"]
"hallo moto"

Prelude> concat [[1..3], [5..6]]
[1,2,3,5,6]

reverse

reverse反转一个列表:

Prelude> :type reverse
reverse :: [a] -> [a]

Prelude> reverse "morning"
"gninrom"

Prelude> reverse [1..5]
[5,4,3,2,1]

and

and求一个列表中所有元素的

Prelude> :type and
and :: [Bool] -> Bool

Prelude> and [True, False, True]
False

Prelude> and []
True

or

or求一个列表中所有元素的

Prelude> :type or
or :: [Bool] -> Bool

Prelude> or [True, False, True]
True

Prelude> or []
False

Note

注意andor对空列表的返回值是不同的。

all

all检查是否列表总所有元素都符合某个属性:

Prelude> :type all
all :: (a -> Bool) -> [a] -> Bool

Prelude> all odd [1, 3 ..10]
True

Prelude> all odd [1..10]
False

any

any检查是否列表中有某个元素符合某个属性:

Prelude> :type any
any :: (a -> Bool) -> [a] -> Bool

Prelude> any odd [1..10]
True

Prelude> any odd [2, 4..10]
False

take

take取列表的前N个值:

Prelude> :type take
take :: Int -> [a] -> [a]

Prelude> take 3 [1..10]
[1,2,3]

Prelude> take 3 [1, 2]
[1,2]

takeWhile

takeWhile只要列表的值符合某个属性,就将其保留,并递归直到遇到第一个不符合属性的值为止。

Prelude> :type takeWhile
takeWhile :: (a -> Bool) -> [a] -> [a]

Prelude> takeWhile odd ([1, 3, 5] ++ [2, 4, 6])
[1,3,5]

Prelude> takeWhile odd [2, 4..10]
[]

drop

drop丢弃列表的前N个值,然后取剩下的值:

Prelude> :type drop
drop :: Int -> [a] -> [a]

Prelude> drop 3 [1..10]
[4,5,6,7,8,9,10]

Prelude> drop 3 [1, 2]
[]

dropWhile

dropWhile只要列表的值符合某个属性,就将其丢弃,并递归直到遇到第一个不符合属性的值为止。

Prelude> :type dropWhile
dropWhile :: (a -> Bool) -> [a] -> [a]

Prelude> dropWhile odd ([1, 3, 5] ++ [2, 4, 6])
[2,4,6]

Prelude> dropWhile odd [2, 4..10]
[2,4,6,8,10]

在列表中搜索

elem

elem查看指定元素是否是列表的值:

Prelude> :type elem
elem :: Eq a => a -> [a] -> Bool

Prelude> 2 `elem` [1..10]
True
notElem

notElem查看指定元素是否不是列表的值:

Prelude> :type notElem
notElem :: Eq a => a -> [a] -> Bool

Prelude> 2 `notElem` [1, 3.10]
True
filter

filter过滤列表,只保留符合给定属性的值:

Prelude> :type filter
filter :: (a -> Bool) -> [a] -> [a]

Prelude> filter odd [1..10]
[1,3,5,7,9]
isPrefixOf, isInfixOf, isSuffixOf

Data.List模块中的isPrefixOfisInfixOfisSuffixOf分别检查某个给定值在列表中的前面、中间或后面:

Prelude> :module +Data.List

Prelude Data.List> :type isPrefixOf
isPrefixOf :: Eq a => [a] -> [a] -> Bool

Prelude Data.List> "good" `isPrefixOf` "good morninig, sir"
True

Prelude Data.List> :type isInfixOf
isInfixOf :: Eq a => [a] -> [a] -> Bool

Prelude Data.List> "morning" `isInfixOf` "good morning, sir"
True

Prelude Data.List> :type isSuffixOf
isSuffixOf :: Eq a => [a] -> [a] -> Bool

Prelude Data.List> "sir" `isSuffixOf` "good morning, sir"
True

一次处理多个列表

zip

zip允许在2个列表中,每次抽取列表中的一个元素,进行组合操作:

Prelude Data.List> :type zip
zip :: [a] -> [b] -> [(a, b)]

Prelude Data.List> zip [1, 3, 5] [2, 4, 6]
[(1,2),(3,4),(5,6)]
zipWith

zipWith可以指定zip执行的操作:

Prelude Data.List> :type zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]

Prelude Data.List> zipWith (+) [1, 2, 3] [4, 5, 6]
[5,7,9]

Note

注意zipzipWith只能处理两个列表,要处理三个列表,要使用zip3zipWith3,以此类推,最高到zip7zipWith7

匿名函数

p.99-100

有时候我们需要一些“一次性”函数来帮助完成一些问题。

比如有时你需要一个将某个值乘以2的函数,于是你定义:

Prelude> let mul_by_2 value = value * 2

Prelude> mul_by_2 10
20

这个方法有一个问题:这个“一次性”函数污染了命名空间,如果这类一次性函数很多的话,就会极大地影响程序的可读性。

我们可以用一个称之为匿名函数(anonymous function / lambda function)的技术来解决这个问题,匿名函数专门用来定义“一次性”函数,而且它和名字绑定,不会污染命名空间。

匿名函数的语法格式如下:

\para1, para2, ..., paraN -> expression

符号\被看作是lambda,后面跟各个形参para1, para2, ..., paraN->之后是函数的表达式。

于是我们可以将之前的mul_by_2函数修改成匿名函数:

Prelude> ((\value -> value * 2) 10)
20

Note

你应该只将“一次性使用”的函数定义为匿名函数,如果一个计算模式多次重复出现,你应该将它定义为一个函数或利用其他组合方式来重用,而不是反复地定义相同(或相类似)的匿名函数,这样可读性只会不增反降。

柯里化(Currying)与偏函数(partial function)

p.100-103

在计算机科学中,柯里化(Currying)指的是将一个接受多个参数的函数变换成一个只接受一个参数的新函数,当有参数传入这个新函数之后,这个新函数又返回一个新函数,以此类推,直到所有参数都被传入之后,函数返回之前通过多个参数计算出来的值。

柯里化一个显著用处就是用来实现偏函数(partial function):我们可以给一个函数传入比它所需参数数目更少的参数(比如一个函数需要三个参数,但我们只付给两个参数),这样函数就不会立即计算值(因为所需的参数还没满足),而是返回一个已经接受了两个参数的新函数。

举个例子,现在我们有一个将三个值组合成一个元组的函数three,定义如下:

three a b c = (a, b, c)

我们查看它的类型签名:

Prelude> :type three
three :: t -> t1 -> t2 -> (t, t1, t2)

现在看看我们将一个参数付给add_3之后,类型签名有什么变化:

Prelude> :type three 'a'
three('a') :: t1 -> t2 -> (Char, t1, t2)

嗯,函数签名里的t不见了,而且元组的类型也被Char占住了,肯定有什么发生了——但是我们先不深究,先继续给three传值,这次我们将两个参数赋值给three

Prelude> :type three 'a' 'b'
three 'a' 'b' :: t2 -> (Char, Char, t2)

噢噢,这次连t1都不见了,你肯定想知道如果把三个参数都传进three会怎么样,马上来做这个:

Prelude> :type three 'a' 'b' 'c'
three 'a' 'b' 'c' :: (Char, Char, Char)

Prelude> three 'a' 'b' 'c'
('a','b','c')

OK,这次tt1t2全都不见了,只剩下三个Char在风中飘零,而且,这次我们可以将函数的值求出来了。

看看上面的类型签名,可以发现每次我们添加一个参数,类型签名里面的变量(比如t)就减少一个:

Prelude> :type three
three :: t -> t1 -> t2 -> (t, t1, t2)

Prelude> :type three 'a'
three 'a' :: t1 -> t2 -> (Char, t1, t2)

Prelude> :type three 'a' 'b'
three 'a' 'b' :: t2 -> (Char, Char, t2)

Prelude> :type three 'a' 'b' 'c'
three 'a' 'b' 'c' :: (Char, Char, Char)

秘密就隐藏在类型签名里面!让我们仔细地研究它们,从参数最多的开始:

three 'a' 'b' 'c' :: (Char, Char, Char)开头的three 'a' 'b' 'c'是一个表达式,表示函数three接受了'a''b'c三个参数,而后面的(Char, Char, Char)表示函数的值是一个三元组,三个元组里面的值都是Char类型,最后,在中间的::分隔开函数表达式和函数返回值类型。

一切顺利,接着开始分析:

three 'a' 'b' :: t2 -> (Char, Char, t2),这个签名的前半部和上面的签名不同,它有两个参数,另外,在签名后半部分t2 -> (Char, Char, t2),和上面的签名对比,我们可以知道t2代表的就是最后一个函数,另外,->是一个新符号,它代表什么?看起来,它的意思似乎是“接受一个参数,然后返回函数的值”。

整个函数签名的意思似乎是:函数three已经接受了两个参数,只要再给它一个参数,它就可以返回计算值了。

嗯,似乎说得通,把这个问题先放一放,先看看下一个签名:

three 'a' :: t1 -> t2 -> (Char, t1, t2),签名前半部和之前的一样,少了一个参数,关键是在后面:->符号两次出现了,之前我们猜测它的意思是“接受一个参数,然后返回一个值”,可这里怎么有两个->符号?难道我们猜错了吗?

其实我们并没有猜错,表达式three 'a'的确是接受一个参数,然后返回一个值,但是这个值不是计算结果,而是一个函数。

回到前面,我们的three接受三个参数:

Prelude> :type three
three :: t -> t1 -> t2 -> (t, t1, t2)

Prelude> three 'a' 'b' 'c'
('a','b','c')

当我们只将一个参数值赋值three的时候,three返回一个新函数,这个新函数接受两个值作为它的参数:

Prelude> :type three 'a'
three 'a' :: t1 -> t2 -> (Char, t1, t2)

当我们再将一个参数赋值给three的适合,three又返回一个新函数,这次这个函数只接受一个值(最后一个,t2)。假如我们再将最后一个参数也赋值给three,那么我们就得到它的计算结果。

实际上:

Prelude> three 'a' 'b' 'c'
('a','b','c')

是由三个函数分别构成的:

Prelude> (((three 'a') 'b') 'c')
('a','b','c')

three首先接受'a'作为它的参数,形成(three 'a'),这个表达式返回一个新的函数,然后这个函数接受又一个值'b',形成表达式((three 'a') 'b'),这个表达式又返回一个新函数,再接受值c,这一次,three的三个函数都齐备了,于是这个函数(不是three,而是接受了两个参数形成的新函数)接收参数'c',并计算出值,然后返回结算结果。

这样以来,为什么three的类型签名有三个->符号也说得过去了——three函数由三个函数组成,它们接受参数、逐次变换并返回新函数,最终组成一个完整的three函数,最后求出结果。

Prelude> :type three
three :: t -> t1 -> t2 -> (t, t1, t2)

这种将一个需要多个参数的函数变换成多个只接受单一参数的函数的过程,称之为柯里化,而那些柯里化生成出来的没有接受到完整参数的函数,称之为偏函数。

偏函数的应用

偏函数的作用很多,其中一个是为一个需要多个参数的函数指定一个参数,然后生成一个新函数,将这个新函数应用到多个地方。

举个例子,我们之前学习了take函数,这个函数用来获取列表的前N项,它有两个参数:一个是获取项的数目,另一个是获取的列表对象。

假设现在我们有一个博客程序,这个程序有好几个列表,包括日志列表(post_list)、评论列表(comment_list)、友链接列表(link_list),等等,如果我们想要多次地提取多个列表的前10个项,我们可能会写这样的代码:

take 10 post_list
take 10 comment_list
take 10 link_list

而利用偏函数,我们可以生成一个新函数,称之为take_10

take_10 list = take 10 list

这样我们就可以将上面的语句改写成下面的形式了:

take_10 post_list
take_10 comment_list
take_10 link_list

噢噢。。。我已经听到有人叫起来了,这个函数的定义并不是偏函数,它只是一个普通的函数抽象,并没有返回一个新函数——的确如此,即使在不支持偏函数的语言,比如Python中,你也可以写这样的代码来获取列表前十个项:

# python code
def take_10(list):
    return list[:10]

好吧,既然我的邪恶计划已经败露,那只好动点真格,写一个真正的偏函数:

take_10_partial = take 10

好的,这个是真的偏函数了,take_10_partial函数返回一个函数,这个函数接受一个列表作参数,用于获取列表的前10个项:

Prelude> :type take_10_partial
take_10_partial :: [a] -> [a]

Prelude> take_10_partial [1..]
[1,2,3,4,5,6,7,8,9,10]

这样,我们就不必每次都将10显式传值给take函数了。

当然这只是偏函数的一个小例子,但从这个例子可以看到,通过适当地使用偏函数,可以让我们避免频繁地重复传值,并且更容易地重用函数。

See also

维基百科的Partial FunctionCurrying是关于偏函数和柯里化的很好的参考。

Note

Python可以通过functools中的partial函数来实现偏函数的效果,但是Python本身是不支持偏函数的。

关于

这个是我个人关于《Real World Haskell》的读书笔记,希望对你也有用。

我会随着阅读的进度更新这个笔记。

那里找到《Real World Haskell》?

《Real World Haskell》可以在官方主页免费阅读(英文),也可以购买东南大学的影印版(英文)

听说清华大学出版社正在翻译中文版,但是翻译了两年似乎还没见踪影。

我个人买的是东南大学的影印版,质量很好,推荐购买。

联系我

gmail/gtalk: huangz1990
twitter: @huangz1990
douban: i_m_huangz