使用Haskell思考
初学Haskell的人需要迈过两大难关:
- 需要将自己的编程观念从命令式编程语言转换到函数式编程语言上来,这样做的原因并不是命令式编程语言不好,而是比起命令式编程语言,函数式更胜一筹.
- 需要熟悉Haskell的标准库.
一个简单的命令行程序
在本章的大部分时间里,我们都之和无副作用的代码打交道,为了将注意力集中在实际的代码上,我们需要开发一个接口程序,连接起带副作用的代码和无副作用的代码.
这个接口程序读入一个文件,将函数应用到文件,并且将结果应用到另一个文件:
-- file: ch04/InteractWith.hs
import System.Environment (getArgs)
interactWith function inputFile outputFile = do
input <- readFile inputFile
writeFile outputFile (function input)
main = mainWith myFunction
where mainWith function = do
args <- getArgs
case args of
[input,output] -> interactWith function input output
_ -> putStrLn "error: exactly two arguments needed"
-- replace "id" with the name of our function below
myFunction = id
do关键字引入一个块,标识那些带副作用的代码,比如对文件进行读和写操作.被do包围的 <- 操作符等同于赋值.
当我们需要测试其他函数时,就将 id 换成其他函数的名字,那些被测试的函数的类型包含 String -> String,即这些函数接收并返回字符串.
然后编译这个程序:
$ ghc --make InteractWith
[1 of 1] Compiling Main ( InteractWith.hs, InteractWith.o )
Linking InteractWith ...
通过命令行运行这个程序,它接收两个文件名作为参数输入,一个用于读取,一个用于写入:
$ echo "hello world" > hello-in.txt
$ ./InteractWith hello-in.txt hello-out.txt
$ cat hello-in.txt
hello world
$ cat hello-out.txt
hello world
热身,方便的分离多行文本
内建函数lines能够很方便的在行边界上分离一段文本字符串,返回一个忽略了行终止符的字符串列表:
ghci> :type lines
lines :: String -> [String]
ghci> lines "line 1\nline 2"
["line 1","line 2"]
ghci> lines "foo\n\nbar\n"
["foo","","bar"]
但是lines函数不会处理行末尾的回车符只会处理换行符:
ghci> lines "a\r\nb"
["a\r","b"]
因此我们需要实现一个自己的函数来支持对回车符的识别:
-- file: ch04/SplitLines.hs
splitLines :: String -> [String]
该函数类型签名标识它接收一个字符串,可能是一些包含回车符或换行符的文本内容.它返回一个字符串列表,列表的每一项表示文本的每一行内容.
-- file: ch04/SplitLines.hs
splitLines [] = []
splitLines cs =
let (pre, suf) = break isLineTerminator cs
in pre : case suf of
('\r':'\n':rest) -> splitLines rest
('\r':rest) -> splitLines rest
('\n':rest) -> splitLines rest
_ -> []
isLineTerminator c = c == '\r' || c == '\n'
break函数可以把列表分成两部分,把一个函数作为第一个参数,这个函数必须去检查列表中的元素,并且返回一个Bool值来表示列表是否在哪个元素被一分为二.break函数返回一个二元组,即由一个返回Bool值得函数返回True之前的元素构造的列表和剩余的元素构成的列表.
ghci> break odd [2,4,5,6,8]
([2,4],[5,6,8])
ghci> :module +Data.Char
ghci> break isUpper "isUpper"
("is","Upper")
splitLines的第一个子表达式表示,如果匹配到一个空的字符串,不做任何处理.
在第二个自表达式中,首先对输入的字符串应用break函数,前缀就是第一个终止符之前的子字符串,后缀就是整个字符串剩余的部分.后缀中将包含可能存在的终止符.
“pre :”表示将上一步的前缀结果添加到整个结果列表上,然后用一个case表达式去检查后缀,并且case表达式的结果用作(:)函数的第二个元素添加到整个结果列表上.
case表达式中的第一个模式用于匹配以一个回车符紧跟一个换行符开始的字符串,变量rest被绑定的剩余的部分上.
使用ghci更详细的观察集中匹配模式,首先是分割一个不包含任何终止符的字符串:
ghci> splitLines "foo"
["foo"]
ghci> break isLineTerminator "foo"
("foo","")
没有匹配到终止符,后缀为空.因此在splitLines函数中的case表达式将匹配到第四个分支,得到一个空列表,然后添加到整个结果列表上.
ghci> splitLines "foo\r\nbar"
["foo","bar"]
ghci> break isLineTerminator "foo\r\nbar"
("foo","\r\nbar")
会得到一个非空的后缀,在case表达式中会匹配到第一个分支,这个求值结果会把pre绑定到”foo”,suf绑定到”bar”,然后递归调用splitLines,这次会匹配上单独的”bar”:
ghci> splitLines "bar"
["bar"]
结果得到一个头部元素是”foo”,尾部元素是”bar”的列表:
ghci> "foo" : ["bar"]
["foo","bar"]
一个行终止转换程序
现在把splitLines函数和之前的InteractWith联系起来.首先拷贝一份InteractWith源码叫做FixLines.hs,然后把splitLines函数加到FixLines.hs中.
因为我们的函数必须产出生成一个单独的字符串,所以必须把这个表示行的列表拼接起来,Prelude提供了一个unlines函数,他会把一个字符串组成的列表拼接起来,并且每个字符串末尾添加一个换行符:
-- file: ch04/SplitLines.hs
fixLines :: String -> String
fixLines input = unlines (splitLines input)
这时把id函数替换成fixLines函数,然后就可以把fixLines.hs编译成将文本文件中的多种终止符替换成系统特定换行符的可执行程序:
$ ghc --make FixLines
[1 of 1] Compiling Main ( FixLines.hs, FixLines.o )
Linking FixLines ...
中缀函数
通常在Haskell中定义或应用一个函数的时候,首先写函数的名称,然后是它的参数.这种表示方法作为前缀表示法.
如果一个函数或构造器带有多个参数,我们可以使用中缀模式,即我们把函数名放在它的第一个和第二个参数之间,以允许我们把该函数作为中缀操作符使用.
要用中缀表示法定义使用函数或构造器,使用重音符(反引号)将该名称扩起:
a `plus` b = a + b
data a `Pair` b = a `Pair` b
deriving (Show)
-- we can use the constructor either prefix or infix
foo = Pair 1 2
bar = True `Pair` "quux"
中缀表示法只是为了语法上的便利而不会改变函数的行为:
ghci> 1 `plus` 2
3
ghci> plus 1 2
3
ghci> True `Pair` "something"
True `Pair` "something"
ghci> Pair True "something"
True `Pair` "something"
在Data.List模块中的一些函数会发现很多明显的改进,isPrefixOf判断一个列表是否匹配另一个列表的开始部分:
ghci> :module +Data.List
ghci> "foo" `isPrefixOf` "foobar"
True
使用列表
基本列表操作
length获取元素数量:
ghci> :type length
length :: [a] -> Int
ghci> length []
0
ghci> length [1,2,3]
3
ghci> length "strings are lists, too"
22
null判断是否为空:
ghci> :type null
null :: [a] -> Bool
ghci> null []
True
ghci> null "plugh"
False
获取第一个元素:
ghci> :type head
head :: [a] -> a
ghci> head [1,2,3]
1
tail获取除了第一个元素的剩余元素:
ghci> :type tail
tail :: [a] -> [a]
ghci> tail "foo"
"oo"
last返回最后一个元素:
ghci> :type last
last :: [a] -> a
ghci> last "bar"
'r'
init返回除了最后一个元素的所有元素:
ghci> :type init
init :: [a] -> [a]
ghci> init "bar"
"ba"
有些函数应用到空列表是会报错.
异常处理
使用head操作空列表是会报错,如果操作前进行非空检查,则可以有效避免错误:
myDumbExample xs = if length xs > 0
then head xs
else 'Z'
或者使用null来更有效的判断列表非空:
-- file: ch04/EfficientList.hs
mySmartExample xs = if not (null xs)
then head xs
else 'Z'
myOtherExample (x:_) = x
myOtherExample [] = 'Z'
更多列表操作
(++)表示追加函数:
ghci> :type (++)
(++) :: [a] -> [a] -> [a]
ghci> "foo" ++ "bar"
"foobar"
ghci> [] ++ [1,2,3]
[1,2,3]
ghci> [True] ++ []
[True]
concat讲一个嵌套列表展开成一个单一列表:
ghci> :type concat
concat :: [[a]] -> [a]
ghci> concat [[1,2,3], [4,5,6]]
[1,2,3,4,5,6]
每次concat调用会去掉最外层的嵌套:
ghci> concat [[[1,2],[3]], [[4],[5],[6]]]
[[1,2],[3],[4],[5],[6]]
ghci> concat (concat [[[1,2],[3]], [[4],[5],[6]]])
[1,2,3,4,5,6]
reverse翻转列表元素顺序:
ghci> :type reverse
reverse :: [a] -> [a]
ghci> reverse "foo"
"oof"
对于包含Bool值得列表,and 和 or相当于编列列表并进行两两求值:
ghci> :type and
and :: [Bool] -> Bool
ghci> and [True,False,True]
False
ghci> and []
True
ghci> :type or
or :: [Bool] -> Bool
ghci> or [False,False,False,True,False]
True
ghci> or []
False
all和any可以操作任何类型的列表,第一个参数接收一个谓词函数,列表作为第二个参数,如果谓词函数对每个元素的判断都为真,all函数返回True,如果至少有一个为真,any返回True.
ghci> :type all
all :: (a -> Bool) -> [a] -> Bool
ghci> all odd [1,3,5]
True
ghci> all odd [3,1,4,1,5,9,2,6,5]
False
ghci> all odd []
True
ghci> :type any
any :: (a -> Bool) -> [a] -> Bool
ghci> any even [3,1,4,1,5,9,2,6,5]
True
ghci> any even []
False
产生子列表
take获取列表前几个元素组成的子列表,drop则丢弃前几个元素返回剩余元素的子列表:
ghci> :type take
take :: Int -> [a] -> [a]
ghci> take 3 "foobar"
"foo"
ghci> take 2 [1]
[1]
ghci> :type drop
drop :: Int -> [a] -> [a]
ghci> drop 3 "xyzzy"
"zy"
ghci> drop 1 []
[]
splitAt函数组合了take和drop的功能,根据给定的索引将原列表切成两个子列表,组成一个二元组返回:
ghci> :type splitAt
splitAt :: Int -> [a] -> ([a], [a])
ghci> splitAt 3 "foobar"
("foo","bar")
takeWhile和dropWhile函数带着谓词函数:takeWhile从开头遍历列表,抽取谓词函数判断为True的元素构成一个新列表,dropWhile则是丢弃谓词函数判断为True的元素并返回剩余元素的列表,两者都是在到达第一个False即停止.
ghci> :type takeWhile
takeWhile :: (a -> Bool) -> [a] -> [a]
ghci> takeWhile odd [1,3,5,6,8,9,11]
[1,3,5]
ghci> :type dropWhile
dropWhile :: (a -> Bool) -> [a] -> [a]
ghci> dropWhile even [2,4,6,7,9,10,12]
[7,9,10,12]
break提取列表中使谓词函数失败的元素组成二元组的首项,而span提取列表中使谓词函数成功的元素组成二元组的首项:
ghci> :type span
span :: (a -> Bool) -> [a] -> ([a], [a])
ghci> span even [2,4,6,7,9,10,11]
([2,4,6],[7,9,10,11])
ghci> :type break
break :: (a -> Bool) -> [a] -> ([a], [a])
ghci> break even [1,3,5,6,8,9,10]
([1,3,5],[6,8,9,10])
搜索列表
elem用于判断一个值是否存在于列表, notElem相反:
ghci> :type elem
elem :: (Eq a) => a -> [a] -> Bool
ghci> 2 `elem` [5,3,2,1,1]
True
ghci> 2 `notElem` [5,3,2,1,1]
False
filter需要提供一个谓词函数,返回列表中判断为True的元素:
ghci> :type filter
filter :: (a -> Bool) -> [a] -> [a]
ghci> filter odd [2,4,1,3,6,8,5,7]
[1,3,5,7]
isPrefixOf用于判断左边的列表是否出现在右边列表的开始处:
ghci> :module +Data.List
ghci> :type isPrefixOf
isPrefixOf :: (Eq a) => [a] -> [a] -> Bool
ghci> "foo" `isPrefixOf` "foobar"
True
ghci> [1,2] `isPrefixOf` []
False
isInfixOf判断左边的列表是否是右边列表的子列表:
ghci> :module +Data.List
ghci> [2,6] `isInfixOf` [3,1,4,1,5,9,2,6,5,3,5,8,9,7,9]
True
ghci> "funk" `isInfixOf` "sonic youth"
False
isSuffixOf用于判断左边的列表时候出现在右边列表的尾部:
ghci> :module +Data.List
ghci> ".c" `isSuffixOf` "crashme.c"
True
同时处理多个列表
zip函数把两个列表压缩成一个单一的由二元组组成的列表,较长列表中多余的元素会被丢弃:
ghci> :type zip
zip :: [a] -> [b] -> [(a, b)]
ghci> zip [12,72,93] "zippity"
[(12,'z'),(72,'i'),(93,'p')]
zipWith需要提供一个函数,用于操作分别从两个列表提取的一对元素,同样较长列表的多余元素会被丢弃:
ghci> :type zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
ghci> zipWith (+) [1,2,3] [4,5,6]
[5,7,9]
特殊字符串处理函数
lines与unlines函数,unlines函数总是在他处理的结果的尾部放一个换行符:
ghci> lines "foo\nbar"
["foo","bar"]
ghci> unlines ["foo", "bar"]
"foo\nbar\n"
words利用任何空白字符分割一个字符串,对应的是unwords函数,使用空格将一个字符串列表串联起来:
ghci> words "the \r quick \t brown\n\n\nfox"
["the","quick","brown","fox"]
ghci> unwords ["jumps", "over", "the", "lazy", "dog"]
"jumps over the lazy dog"
循环的表示
Haskell中没有for循环或while循环等loop.
显式递归
首先定义类型签名:
-- file: ch04/IntParse.hs
import Data.Char (digitToInt) -- we'll need ord shortly
asInt :: String -> Int
loop :: Int -> String -> Int
asInt xs = loop 0 xs
loop 的第一个参数是累积器的变量,给他赋值0等同于在C语言中for循环的初始化操作.
我们需要处理的数据是:输入xs是一个包含数字的字符串,而String类型是[Char]的别名,一个包含字符的列表.因此需要遍历字符串是可以把他当做列表来处理:要么是一个空字符,要么是一个字符,后面跟着列表的剩余部分.
因此可以通过列表的构造器进行模式匹配来表达,首先匹配空字符串:
loop acc [] = acc
一个空列表并不仅仅意味着”输入了一个空列表”,另一层含义是,对一个空列表遍历并到达了列表的末尾,因此对于空列表,不能抛出错误,而是将值累加.
另一个等式处理列表不为空的情况,先取出并处理当前元素,接着处理列表的剩余元素:
loop acc (x:xs) = let acc' = acc * 10 + digitToInt x
in loop acc' xs
程序首先计算当前字符代表的数值,将它赋值给变量acc’,然后使用acc’和剩余列表的元素xs作为参数,再次调用loop函数,等同于在C语言中再次执行一次循环.
每次调用loop,累积器的值都会被更新,并处理掉列表里的一个元素,直到列表为空,递归调用结束.下面是IntParse的完整定义:
-- file: ch04/IntParse.hs
-- 只载入 Data.Char 中的 digitToInt 函数
import Data.Char (digitToInt)
asInt xs = loop 0 xs
loop :: Int -> String -> Int
loop acc [] = acc
loop acc (x:xs) = let acc' = acc * 10 + digitToInt x
in loop acc' xs
然后在ghci中测试:
Prelude> :load IntParse.hs
[1 of 1] Compiling Main ( IntParse.hs, interpreted )
Ok, modules loaded: Main.
*Main> asInt "33"
33
对列表元素进行转换
对数组中的所有元素进行平方计算:
-- file: ch04/square.hs
square :: [Double] -> [Double]
square (x:xs) = x*x : square xs
square [] = []
square函数包含两个模式.第一个模式将接收到的参数-一个列表结构,取出他的head和tail部分,并对第一个元素进行加倍操作,然后将计算所得的结果添加到列表,同时递归调用square操作剩余部分的元素,直到列表为空,递归结束.
square产生一个和输入列表长度一致的列表,其中每个新元素都是原本元素的平方:
Prelude> :load square.hs
[1 of 1] Compiling Main ( square.hs, interpreted )
Ok, modules loaded: Main.
*Main> let one_to_ten = [1.0 .. 10.0]
*Main> square one_to_ten
[1.0,4.0,9.0,16.0,25.0,36.0,49.0,64.0,81.0,100.0]
将字符串中的的所有字母转换为大写形式:
-- file: ch04/upperCase.hs
import Data.Char (toUpper)
upperCase :: String -> String
upperCase (x: xs) = toUpper x : upperCase xs
upperCase [] = []
以上两个函数遵循了同一种处理列表的公共模式:基本情形(base case)处理空列表,递归情形(recursive case)处理非空列表,首先对列表的头元素进行操作,然后递归处理列表余下的其他元素.
列表映射
square和upperCase函数都生成一个很原有列表长度相同的列表,并且每次只对一个元素进行处理,因为这种用法很常见,因此Haskell的Prelude库定义了一个map操作.
map函数接收一个函数和一个列表作为参数,将输入函数应用到列表的每一个元素上从而生成一个新的列表:
-- file: ch04/rewrite_by_map.hs
import Data.Char (toUpper)
square2 xs = map squareOne xs
where squareOne x = x * x
upperCase2 xs = map toUpper xs
通过map函数的类型签名观察它是如何实现的:
Prelude> :type map
map :: (a -> b) -> [a] -> [b]
该签名显示,map接收两个参数,第一个参数是一个函数,接收一个a类型的值,并返回一个b类型的值.
实现一个自己的map函数:
-- file: ch04/myMap.hs
myMap :: (a -> b) -> [a] -> [b]
myMap f (x:xs) = f x : myMap f xs
myMap _ [] = []
筛选列表元素
一下函数接收一个列表作为参数,并返回列表中的奇数元素.使用守卫对元素进行判断,只有符合条件的元素才会被添加进结果列表:
-- file: ch04/oddList.hs
oddList :: [Int] -> [Int]
oddList (x:xs) | odd x = x : oddList xs
| otherwise = oddList xs
oddList [] = []
因为这种用法常见,因此Prelude定义了filter函数:接收一个谓词函数,并应用到列表的每个元素,只有谓词函数求值结果为True的元素被添加到结果列表:
Prelude> :type odd
odd :: Integral a => a -> Bool
Prelude> odd 1
True
Prelude> odd 2
False
Prelude> :type filter
filter :: (a -> Bool) -> [a] -> [a]
Prelude> filter odd [1 .. 10]
[1,3,5,7,9]
处理集合并得出结果
将一个集合(collection)缩减(reduce)成一个值也是常见的需求.
比如对列表的元素求和:
-- file: ch04/mySum.hs
mySum xs = helper 0 xs
where helper acc (x:xs) = helper (acc + x) xs
helper acc [] = acc
helper函数通过尾递归进行计算,acc是累加器,它记录了当前列表元素的的总和.
Adler-32算法:将连个16位校验和串联成一个32位校验和.第一个校验和是所有输入比特之和,而第二个校验和则是第一个校验和所有中间值之和,每次计算时,校验和都需要取模65521.
使用Haskell实现Adler-32算法:
-- file: ch04/Adler32.hs
import Data.Char (ord)
import Data.Bits (shiftL, (.&.), (.|.))
base = 65521
adler32 xs = helper 1 0 xs
where helper a b (x:xs) = let a' = (a + (ord x .&. 0xff)) `mod` base
b' = (a' + b) `mod` base
in helper a' b' xs
helper a b [] = (b `shiftL` 16) .|. a
shiftL 函数实现逻辑左移,(.&.) 实现二进制位的并操作,(.|.) 实现二进制位的或操作,ord 函数则返回给定字符对应的编码值.
可以抽象出一种通用的抽象,称之为折叠(fold):对一个列表中的所有元素做某种处理,并且一边处理元素,一边更新累积器,最后在处理完整个列表之后,返回累计器的值.
有两种不同类型的折叠,foldl从左边开始进行折叠,而foldr从右边开始折叠.
左折叠
foldl的定义:
-- file: ch04/foldl.hs
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl step zero (x:xs) = foldl step (step zero x) xs
foldl _ zero [] = zero
foldl接收一个步骤(step)函数,一个累计器的初始化值,以及一个列表作为参数.步骤函数每次使用累计器和列表中的一个元素作为参数,并计算出新的累积器值,这个过程称为步进,然后将新的累加器作为参数,再次进行同样的计算,直到整个列表被处理完为止.
以下是foldl函数重写mySum函数:
-- file: ch04/foldlSum.hs
foldlSum xs = foldl step 0 xs
where step acc x = acc + x
因为step函数执行的操作只不过是将两个输入参数相加,因此,可以直接将一个加法函数代替step函数,并移除多余的where语句:
-- file: ch04/niceSum.hs
niceSum :: [Integer] -> Integer
niceSum xs = foldl (+) 0 xs
为了进一步看清楚foldl的执行模式,以下代码展示了niceSum [1,2,3]执行时的过程:
niceSum [1, 2, 3]
== foldl (+) 0 (1:2:3:[])
== foldl (+) (0 + 1) (2:3:[])
== foldl (+) ((0 + 1) + 2) (3:[])
== foldl (+) (((0 + 1) + 2) + 3) []
== (((0 + 1) + 2) + 3)
新的niceSum定义比之前的mySum定义节省了很多代码:新版本没有使用显示递归,因为foldl可以代替我们搞定循环.现在程序只要求我们回答两个问题:累积器的初始化值是什么,怎么更新累积器的值(+函数).
从右边开始折叠
和foldl相对的就是foldr,它从列表的右边开始执行:
-- file: ch04/foldr.hs
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr step zero (x:xs) = step x (foldr step zero xs)
foldr _ zero [] = zero
使用foldr改写niceSum函数:
-- file: ch04/niceSumFoldr.hs
niceSumFoldr :: [Int] -> Int
niceSumFoldr xs = foldr (+) 0 xs
这个 niceSumFoldr 函数在输入为 [1, 2, 3] 时,产生以下计算序列:
niceSumFoldr [1, 2, 3]
== foldr (+) 0 (1:2:3:[])
== 1 + foldr (+) 0 (2:3:[])
== 1 + (2 + foldr (+) 0 (3:[]))
== 1 + (2 + (3 + foldr (+) 0 []))
== 1 + (2 + (3 + 0))
可以观察到foldr与foldl的不同:foldl将初始化值放在左边,括号也是从左边开始包围.foldr将初始化值放在右边,而括号也是从右边开始包围.
匿名(lambda)函数
在前面定义的函数中,很多函数都带有一个简单的辅助函数:
-- file: ch04/isInAny.hs
import Data.List (isInfixOf)
isInAny needle haystack = any inSequence haystack
where inSequence s = needle `isInfixOf` s
Haskell中匿名函数以反斜杠 \ 为开始,后跟函数的参数(可以包含模式),而函数体定义在 -> 符号之后,反斜杠读作lambda.
使用lambda改写isInAny:
-- file: ch04/isInAny2.hs
import Data.List (isInfixOf)
isInAny2 needle haystack = any (\s -> needle `isInfixOf` s) haystack
使用括号包围整个lambda函数,以便Haskell知道函数结束的位置.
部分函数应用和柯里化
类型签名中的多个 -> 符号可能使人迷惑:
Prelude> :type dropWhile
dropWhile :: (a -> Bool) -> [a] -> [a]
似乎 -> 用于隔开函数的多个参数(括号里的 a 和 Bool ),有用于隔开函数参数和函数返回值类型((a -> Bool) -> [a] 和 [a]).
但是 -> 符号只表示一种用途:它表示一个函数接收一个参数,并返回一个值.其中 -> 左边是参数的类型,右边是返回值的类型.
理解该符号的意义非常重要:Haskell中,所有函数都只接受一个参数.尽管dropWhile看上去像是接收两个参数的函数,但实际上它只接收一个参数,而这个函数的返回值是另一个函数,这个被当做返回值返回的函数也只接收一个参数.
下面是一个完全合法的表达式:
Prelude> :module +Data.Char
Prelude Data.Char> :type dropWhile isSpace
dropWhile isSpace :: [Char] -> [Char]
表达式dropWhile isSpace的值是一个函数,这个函数移除了一个字符串的所有前缀空白,可以把它应用到一个高阶函数:
Prelude Data.Char> map (dropWhile isSpace) [" a", "f", " e"]
["a","f","e"]
每当将一个参数传递给函数时,这个函数的类型签名最前面的一个元素就会被”移除掉”.使用zip3函数作为实例,这个函数接收3个列表,并将他们压缩成一个包含三元组的列表:
Prelude> :type zip3
zip3 :: [a] -> [b] -> [c] -> [(a, b, c)]
Prelude> zip3 "foo" "bar" "quux"
[('f','b','q'),('o','a','u'),('o','r','u')]
如果将一个参数应用到zip3函数,那么他将返回一个接收两个参数的函数.无论之后将什么参数传递给这个复合函数,第一次传入的参数的值都不会改变.
Prelude> :type zip3
zip3 :: [a] -> [b] -> [c] -> [(a, b, c)]
Prelude> :type zip3 "foo"
zip3 "foo" :: [b] -> [c] -> [(Char, b, c)]
Prelude> :type zip3 "foo" "bar"
zip3 "foo" "bar" :: [c] -> [(Char, Char, c)]
Prelude> :type zip3 "foo" "bar" "quux"
zip3 "foo" "bar" "quux" :: [(Char, Char, Char)]
如果传入参数的数量少于函数能够接收的参数的数量,则被称为函数的部分应用(partial application of the function),函数正被他的其中几个参数应用.
上面的例子中,zip3 “foo”就是一个部分应用函数,它以”foo”作为第一个参数,部分应用了zip3函数.同理zip3 “foo” “bar”.
只要给部分函数补充上足够的参数,它就能够成功求值:
Prelude> let zip3foo = zip3 "foo"
Prelude> zip3foo "bar" "quux"
[('f','b','q'),('o','a','u'),('o','r','u')]
Prelude> let zip3foobar = zip3 "foo" "bar"
Prelude> zip3foobar "quux"
[('f','b','q'),('o','a','u'),('o','r','u')]
Prelude> zip3foobar [1, 2, 3]
[('f','b',1),('o','a',2),('o','r',3)]
节
Haskell提供了一种方便的符号快捷方式,用于对中序函数进行部分应用:它使用括号包围一个操作符,通过在括号里提供一个做操作对象或右操作对象,可以产生一个部分应用函数,这种类型的部分函数应用称为节(section).
Prelude> (1+) 2
3
Prelude> map (*3) [24, 36]
[72,108]
Prelude> map (2^) [3, 5, 7, 9]
[8,32,128,512]
如果向节提供左操作对象,那么得出的部分应用就会将得到的参数应用为右操作对象,反之亦然.
As-模式
Data.List 模块里定义的 tails 函数是 tail 的推广,他返回一个列表的所有”尾巴”:
Prelude> :m +Data.List
Prelude Data.List> tail "foobar"
"oobar"
Prelude Data.List> tail (tail "foobar")
"obar"
Prelude Data.List> tails "foobar"
["foobar","oobar","obar","bar","ar","r",""]
tails 返回一个包含字符串的列表,这个列表保存了输入字符串的所有后缀,以及一个额外的空列表(放在结果列表的最后),tails 的返回值总是带有额外的空列表,即使它的输入为空时:
Prelude Data.List> tails ""
[[]]
如果需要一个行为和tails类似,但不包含空列表后缀的函数,可以如下定义:
-- file: ch04/suffixes.hs
suffixes :: [a] -> [[a]]
suffixes xs@(_:xs') = xs : suffixes xs'
suffixes [] = []
源码里面用到了新引入的 @ 符号,模式 xs@(:xs’) 被称为 as-模式,它的意思是:如果输入值能匹配 @ 符号右边的模式(这里是 (:xs’) ),那么就将这个值绑定到 @ 符号左边的变量中(这里是 xs ).
在这个例子里,如果输入值能够匹配模式 (:xs’) ,那么这个输入值这就被绑定为 xs ,它的 tail 部分被绑定为 xs’ ,而它的 head 部分因为使用通配符 进行匹配,所以这部分没有被绑定到任何变量.
一个不适用AS模式的定义:
-- file: noAsPattern.hs
noAsPattern :: [a] -> [[a]]
noAsPattern (x:xs) = (x:xs) : noAsPattern xs
noAsPattern [] = []
可以发现,使用as-模式的定义同时完成了模式匹配和变量绑定两份工作,而不适用as-模式的定义,则需要在对列表进行结构之后,在函数体里对列表重新进行组合.