《Real World Haskell》读书笔记¶
这个是我个人关于《Real World Haskell》的读书笔记,希望对你也有用。
我会随着阅读的进度更新这个笔记。
目录:
第一章:起步¶
算数¶
Haskell中的算数默认使用中序格式(infix form),也可以将操作符用括号包围,然后使用前序格式(prefix form)。
Prelude> 2 + 2
4
Prelude> (+) 2 2
4
负数¶
当负号操作符-
有两个操作符是,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的类型名必须以大写字母开头(比如Int
,String
),而变量名则必须以小写字母开头:
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
第二章:类型和函数¶
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¶
布尔变量,包括True
和False
。
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比较特殊,作用在它们之上有两个函数:fst
和snd
,它们分别获取元组的头元素和第二元素。
Warning
如果你熟悉Lisp,注意这里的fst
、snd
函数和Lisp里面的car
和cdr
是不同的,Lisp里的car
和cdr
可以作用于任何长度的列表,而Haskell里的fst
和snd
只能作用于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
其中then
和else
之后的表达式称之为分支(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¶
定义一个类型需要几个部分:
- 类型名(type name, type constructor)
- 值构造器(value constructor, data constructor)
- 组成元素(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
比对输入数值,对0
和1
输出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¶
let
和double
不但可以用来定义变量,还可以用来定义函数(其实它们都是表达式):
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
如果predicate
为True
,则执行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_1
,value_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
函数不是比对三个值(3
、5
、 10
),而是一个范围形函数,用模式匹配是没有办法写完这种匹配的(除非你是机器人。。。):
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 2
比plus 1 2
更直观。
但是在Haskell中,直接写1 plus 2
是不行的,Haskell只会认为1
是函数方法,而plus
和2
则是传入给它的参数,最终造成一个错误:
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
head¶
head
返回列表的第一个元素:
Prelude> :type head
head :: [a] -> a
Prelude> head [1, 2, 3]
1
Prelude> head []
*** Exception: Prelude.head: empty list
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
当head
和init
这类函数作用在空列表的时候,会抛出一个错误。
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
注意and
和or
对空列表的返回值是不同的。
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
模块中的isPrefixOf
、isInfixOf
和isSuffixOf
分别检查某个给定值在列表中的前面、中间或后面:
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
注意zip
和zipWith
只能处理两个列表,要处理三个列表,要使用zip3
和zipWith3
,以此类推,最高到zip7
和zipWith7
。
匿名函数¶
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,这次t
、t1
、t2
全都不见了,只剩下三个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 Function和Currying是关于偏函数和柯里化的很好的参考。
Note
Python可以通过functools中的partial函数来实现偏函数的效果,但是Python本身是不支持偏函数的。
关于¶
这个是我个人关于《Real World Haskell》的读书笔记,希望对你也有用。
我会随着阅读的进度更新这个笔记。
那里找到《Real World Haskell》?¶
《Real World Haskell》可以在官方主页免费阅读(英文),也可以购买东南大学的影印版(英文)。
听说清华大学出版社正在翻译中文版,但是翻译了两年似乎还没见踪影。
我个人买的是东南大学的影印版,质量很好,推荐购买。