函数的语法
模式匹配
本章讲的就是haskell那套酷酷的语法结构,先从模式匹配开始。模式匹配通过检查数据的特定结构来检查其是否匹配,并按模式从中取得数据。
在定义函数时,你可以为不同的模式分别定义函数体,这就让代码更加简洁易读。你可以匹配一切数据类型---数字,字符,List,元组,等等。我们弄个简单函数,让它检查我们传给它的数字是不是7。
lucky :: (Integral a) => a -> Stringlucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"
在调用lucky时,模式会从上至下进行检查,一旦有匹配,那对应的函数体就被应用了。这个模式中的唯一匹配是参数为7,如果不是7,就转到下一个模式,它匹配一切数值并将其绑定为x。这个函数完全可以使用if实现,不过我们若要个分辨1到5中的数字,而无视其它数的函数该怎么办?要是没有模式匹配的话,那可得好大一棵if-else树了!
sayMe :: (Integral a) => a -> StringsayMe 1 = "One!"
sayMe 2 = "Two!"
sayMe 3 = "Three!"
sayMe 4 = "Four!"
sayMe 5 = "Five!"
sayMe x = "Not between 1 and 5"
注意下,如果我们把最后匹配一切的那个模式挪到最前,它的结果就全都是"Not between 1 and 5" 了。因为它自己匹配了一切数字,不给后面的模式留机会。
记得前面实现的那个阶乘函数么?当时是把n的阶乘定义成了product [1..n]。也可以写出像数学那样的递归实现,先说明0的阶乘是1,再说明每个正整数的阶乘都是这个数与它前驱(predecessor)对应的阶乘的积。如下便是翻译到haskell的样子:
factorial :: (Integral a) => a -> afactorial 0 = 1
factorial n = n * factorial (n - 1)
这就是我们定义的第一个递归函数。递归在haskell中十分重要,我们会在后面深入理解。如果拿一个数(如3)调用factorial函数,这就是接下来的计算步骤:先计算3*factorial 2,factorial 2等于2*factorial 1,也就是3*(2*(factorial 1))。factorial 1等于1*factorial 0,好,得3*(2*(1*factorial 0)),递归在这里到头了,嗯---我们在万能匹配前面有定义,0的阶乘是1.于是最终的结果等于3*(2*(1*1))。若是把第二个模式放在前面,它就会捕获包括0在内的一切数字,这一来我们的计算就永远都不会停止了。这便是为什么说模式的顺序是如此重要:它总是优先匹配最符合的那个,最后才是那个万能的。
模式匹配也会失败。假如这个函数:
charName :: Char -> StringcharName 'a' = "Albert"
charName 'b' = "Broseph"
charName 'c' = "Cecil"
拿个它没有考虑到的字符去调用它,你就会看到这个:
ghci> charName 'a'"Albert"
ghci> charName 'b'
"Broseph"
ghci> charName 'h'
"*** Exception: tut.hs:(53,0)-(55,21): Non-exhaustive patterns in function charName
它告诉我们说,这个模式不够全面。因此,在定义模式时,一定要留一个万能匹配的模式,这样我们的程序就不会为了不可预料的输入而崩溃了。
对Tuple同样可以使用模式匹配。写个函数,将二维空间中的向量相加该如何?将它们的x项和y项分别相加就是了。如果不了解模式匹配,我们很可能会写出这样的代码:
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)addVectors a b = (fst a + fst b, snd a + snd b)
嗯,可以运行。但有更好的方法,上模式匹配:
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
there we go!好多了!注意,它已经是个万能的匹配了。两个addVector的类型都是addVectors:: (Num a) => (a,a) -> (a,a) -> (a,a),我们就能够保证,两个参数都是序对(Pair)了。
fst和snd可以从序对中取出元素。三元组(Tripple)呢?嗯,没现成的函数,得自己动手:
first :: (a, b, c) -> afirst (x, _, _) = x
second :: (a, b, c) -> b
second (_, y, _) = y
third :: (a, b, c) -> c
third (_, _, z) = z
这里的_就和List Comprehension中一样。表示我们不关心这部分的具体内容。
说到List Comprehension,我想起来在List Comprehension中也能用模式匹配:
ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]ghci> [a+b | (a,b) <- xs]
[4,7,6,8,11,4]
一旦模式匹配失败,它就简单挪到下个元素。
对list本身也可以使用模式匹配。你可以用[]或:来匹配它。因为[1,2,3]本质就是1:2:3:[]的语法糖。你也可以使用前一种形式,像x:xs这样的模式可以将list的头部绑定为x,尾部绑定为xs。如果这list只有一个元素,那么xs就是一个空list。
Note:x:xs这模式的应用非常广泛,尤其是递归函数。不过它只能匹配长度大于等于1的list。
如果你要把list的前三个元素都绑定到变量中,可以使用类似x:y:z:xs这样的形式。它只能匹配长度大于等于3的list。
我们已经知道了对list做模式匹配的方法,就实现个我们自己的head函数。
head' :: [a] -> ahead' [] = error "Can't call head on an empty list, dummy!"
head' (x:_) = x
看看管不管用:
ghci> head' [4,5,6]4
ghci> head' "Hello"
'H'
漂亮!注意下,你若要绑定多个变量(用_也是如此),我们必须用括号将其括起。同时注意下我们用的这个error函数,它可以生成一个运行时错误,用参数中的字符串表示对错误的描述。它会直接导致程序崩溃,因此应谨慎使用。可是对一个空list取head真的不靠谱哇。
弄个简单函数,让它用非标准的英语给我们展示list的前几项。
tell :: (Show a) => [a] -> Stringtell [] = "The list is empty"
tell (x:[]) = "The list has one element: " ++ show x
tell (x:y:[]) = "The list has two elements: " ++ show x ++ " and " ++ show y
tell (x:y:_) = "This list is long. The first two elements are: " ++ show x ++ " and " ++ show y
这个函数顾及了空list,单元素list,双元素list以及较长的list,所以这个函数很安全。(x:[])与(x:y:[])也可以写作[x]和[x,y](有了语法糖,我们不必多加括号)。不过(x:y:_)这样的模式就不行了,因为它匹配的list长度不固定。
我们曾用List Comprehension实现过自己的length函数,现在用模式匹配和递归重新实现它:
length' :: (Num b) => [a] -> blength' [] = 0
length' (_:xs) = 1 + length' xs
这与先前写的那个factorial函数很相似。先定义好未知输入的结果---空list,这也叫作边界条件。再在第二个模式中将这List分割为头部和尾部。说,List的长度就是其尾部的长度加1。匹配头部用的_,因为我们并不关心它的值。同时也应明确,我们顾及了List所有可能的模式:第一个模式匹配空list,第二个匹配任意的非空list。
看下拿"ham"调用length'会怎样。首先它会检查它是否为空List。显然不是,于是进入下一模式。它匹配了第二个模式,把它分割为头部和尾部并无视掉头部的值,得长度就是1+length' "am"。ok。以此类推,"am"的length就是1+length' "m"。好,现在我们有了1+(1+length' "m")。length' "m"即1+length ""(也就是1+length' [])。根据定义,length' []等于0。最后得1+(1+(1+0))。
再实现sum。我们知道空list的和是0,就把它定义为一个模式。我们也知道一个list的和就是头部加上尾部的和的和。写下来就成了:
sum' :: (Num a) => [a] -> asum' [] = 0
sum' (x:xs) = x + sum' xs
还有个东西叫做as模式,就是将一个名字和@置于模式前,可以在按模式分割什么东西时仍保留对其整体的引用。如这个模式xs@(x:y:ys),它会匹配出与x:y:ys对应的东西,同时你也可以方便地通过xs得到整个list,而不必在函数体中重复x:y:ys。看下这个quick and dirty的例子:
capital :: String -> Stringcapital "" = "Empty string, whoops!"
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x] ghci> capital "Dracula"
"The first letter of Dracula is D"
我们使用as模式通常就是为了在较大的模式中保留对整体的引用,从而减少重复性的工作。
还有——你不可以在模式匹配中使用++。若有个模式是(xs++ys),那么这个List该从什么地方分开呢?不靠谱吧。而(xs++[x,y,z])或只一个(xs++[x])或许还能说的过去,不过出于list的本质,这样写也是不可以的。
注意,门卫!
模式用来检查一个值是否合适并从中取值,而门卫(guard)则用来检查一个值的某项属性是否为真。咋一听有点像是if语句,实际上也正是如此。不过处理多个条件分支时门卫的可读性要高些,并且与模式匹配契合的很好。
在讲解它的语法前,我们先看一个用到门卫的函数。它会依据你的BMI值(body mass index,身体质量指数)来不同程度地侮辱你。BMI值即为体重除以身高的平方。如果小于18.5,就是太瘦;如果在18.5到25之间,就是正常;25到30之间,超重;如果超过30,肥胖。这就是那个函数(我们目前暂不为您计算bmi,它只是直接取一个emi值)。
bmiTell :: (RealFloat a) => a -> StringbmiTell bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
门卫由跟在函数名及参数后面的竖线标志,通常他们都是靠右一个缩进排成一列。一个门卫就是一个布尔表达式,如果为真,就使用其对应的函数体。如果为假,就送去见下一个门卫,如之继续。如果我们用24.3调用这个函数,它就会先检查它是否小于等于18.5,显然不是,于是见下一个门卫。24.3小于25.0,因此通过了第二个门卫的检查,就返回第二个字符串。
在这里则是相当的简洁,不过不难想象这在命令式语言中又会是怎样的一棵if-else树。由于if-else的大树比较杂乱,若是出现问题会很难发现,门卫对此则十分清楚。
最后的那个门卫往往都是otherwise,它的定义就是简单一个otherwise = True,捕获一切。这与模式很相像,只是模式检查的是匹配,而它们检查的是布尔表达式 。如果一个函数的所有门卫都没有通过(而且没有提供otherwise作万能匹配),就转入下一模式。这便是门卫与模式契合的地方。如果始终没有找到合适的门卫或模式,就会发生一个错误。
当然,门卫可以在含有任意数量参数的函数中使用。省得用户在使用这函数之前每次都自己计算bmi。我们修改下这个函数,让它取身高体重为我们计算。
bmiTell :: (RealFloat a) => a -> a -> StringbmiTell weight height
| weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
| weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
看看我胖不胖......
ghci> bmiTell 85 1.90"You're supposedly normal. Pffft, I bet you're ugly!"
Yay!我不胖!不过haskell依然说我很猥琐...什么道理...
注意下,函数名和参数的后面并没有=。许多新人容易搞出语法错误,就是因为在后面加上了=。
另一个简单的例子:实现个自己的max函数。应该还记得,它是取两个可比较的值,返回较大的那个。
max' :: (Ord a) => a -> a -> amax' a b
| a > b = a
| otherwise = b
门卫也可以堆一行里面。这样的可读性会差些,因而是不被鼓励的。即使是较短的函数也是如此,仅仅出于演示,我们可以这样重写max':
max' :: (Ord a) => a -> a -> amax' a b | a > b = a | otherwise = b
Ugh!一点都不好读!继续进发,用门卫实现我们自己的compare函数:
myCompare :: (Ord a) => a -> a -> Orderinga `myCompare` b
| a > b = GT
| a == b = EQ
| otherwise = LT ghci> 3 `myCompare` 2
GT
Note:通过反单引号,我们不仅可以以中缀形式调用函数,也可以在定义函数的时候使用它。有时这样会更易读。
Where?
前一节中我们写了这个bmi计算函数:
bmiTell :: (RealFloat a) => a -> a -> StringbmiTell weight height
| weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
| weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
注意,我们重复了3次。我们重复了3次。程序员的字典里不应该有“重复”这个词。既然发现有重复,那么给它一个名字来代替这三个表达式会更好些。嗯,我们可以这样修改:
bmiTell :: (RealFloat a) => a -> a -> StringbmiTell weight height
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
我们的where关键字跟在门卫后面(最好是与竖线缩进一致),可以定义多个名字和函数。这些名字对每个门卫都是可见的,这一来就避免了重复。如果我们打算换种方式计算bmi,只需进行一次修改就行了。通过命名,我们提升了代码的可读性,并且由于bmi只计算了一次,函数的执行效率也有所提升。我们可以再做下修改:
bmiTell :: (RealFloat a) => a -> a -> StringbmiTell weight height
| bmi <= skinny = "You're underweight, you emo, you!"
| bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= fat = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
skinny = 18.5
normal = 25.0
fat = 30.0
函数在where绑定中定义的名字只对本函数可见,因此我们不必担心它会污染其他函数的命名空间。注意,其中的名字都是一列垂直排开,如果不这样规范,haskell就搞不清楚它们在哪个地方了。
where绑定不会在多个模式中共享。如果你在一个函数的多个模式中重复用到同一名字,就应该把它置于全局定义之中。
where绑定也可以使用模式匹配!前面那段代码可以改成:
...where bmi = weight / height ^ 2
(skinny, normal, fat) = (18.5, 25.0, 30.0)
我们再搞个简单函数,让它告诉我们姓名的首字母:
initials :: String -> String -> Stringinitials firstname lastname = [f] ++ ". " ++ [l] ++ "."
where (f:_) = firstname
(l:_) = lastname
我们完全按可以在函数的参数上直接使用模式匹配(这样更短更简洁),在这里只是为了演示在where语句中同样可以使用模式匹配:
where绑定可以定义名字,也可以定义函数。保持健康的编程风格,我们搞个计算一组bmi的函数:
calcBmis :: (RealFloat a) => [(a, a)] -> [a]calcBmis xs = [bmi w h | (w, h)
where bmi weight height = weight / height ^ 2
这就全了!在这里将bmi搞成一个函数,是因为我们不能依据参数直接进行计算,而必须先从传入函数的list中取出每个序对并计算对应的值。
where绑定还可以嵌套。有个已被广泛接受的理念,就是一个函数应该有几个辅助函数。而每个辅助函数也可以通过where拥有各自的辅助函数。
Let it be
let绑定与where绑定很相似。where绑定是在函数底部定义名字,对包括所有门卫在内的整个函数可见。let绑定则是个表达式,允许你在任何位置定义局部变量,而对不同的门卫不可见。正如haskell中所有赋值结构一样,let绑定也可以使用模式匹配。看下它的实际应用!这是个依据半径和高度求圆柱体表面积的函数:
cylinder :: (RealFloat a) => a -> a -> acylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r ^2
in sideArea + 2 * topArea
let的格式为let [bindings] in [expressions]。在let中绑定的名字仅对in部分可见。let里面定义的名字也得对齐到一列。不难看出,这用where绑定也可以做到。那么它俩有什么区别呢?看起来无非就是,let把绑定放在语句前面而where放在后面嘛。
不同之处在于,let绑定本身是个表达式,而where绑定则是个语法结构。还记得前面我们讲if语句时提到它是个表达式,因而可以随处安放?
ghci> [if 5 > 3 then "Woo" else "Boo", if 'a' > 'b' then "Foo" else "Bar"]["Woo", "Bar"]
ghci> 4 * (if 10 > 5 then 10 else 0) + 2
42
用let绑定也可以实现:
ghci> 4 * (let a = 9 in a + 1) + 242
let也可以定义局部函数:
ghci> [let square x = x * x in (square 5, square 3, square 2)][(25,9,4)]
若要在一行中绑定多个名字,再将它们排成一列显然是不可以的。不过可以用分号将其分开。
ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar)(6000000,"Hey there!")
最后那个绑定后面的分号不是必须的,不过加上也没关系。如我们前面所说,你可以在let绑定中使用模式匹配。这在从Tuple取值之类的操作中很方便。
ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100600
你也可以把let绑定放到List Comprehension中。我们重写下那个计算bmi值的函数,用个let替换掉原先的where。
calcBmis :: (RealFloat a) => [(a, a)] -> [a]calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]
List Comprehension中let绑定的样子和限制条件差不多,只不过它做的不是过滤,而是绑定名字。let中绑定的名字在输出函数及限制条件中都可见。这一来我们就可以让我们的函数只返回胖子的bmi值:
calcBmis :: (RealFloat a) => [(a, a)] -> [a]calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]
在(w, h) <- xs这里无法使用bmi这名字,因为它在let绑定的前面。
在List Comprehension中我们忽略了let绑定的in部分,因为名字的可见性已经预先定义好了。不过,把一个let...in放到限制条件中也是可以的,这样名字只对这个限制条件可见。在GHCi中in部分也可以省略,名字的定义就在整个交互中可见。
ghci> let zoot x y z = x * y + zghci> zoot 3 9 2
29
ghci> let boot x y z = x * y + z in boot 3 4 2
14
ghci> boot
< interactive>:1:0: Not in scope: `boot'
你说既然let已经这么好了,还要where干嘛呢?嗯,let是个表达式,定义域限制的相当小,因此不能在多个门卫中使用。一些朋友更喜欢where,因为它是跟在函数体后面,把主函数体距离类型声明近一些会更易读。
case表达式
有命令式编程(C, C++, Java, etc)的经验的同学一定会有所了解,很多命令式语言都提供了case语句。就是取一个变量,按照对变量的判断选择对应的代码块。其中可能会存在一个万能匹配以处理未预料的情况。
haskell取了这一概念融合其中。如其名,case表达式就是,嗯,一种表达式。跟if..else和let一样的表达式。用它可以对变量的不同情况分别求值,还可以使用模式匹配。Hmm,取一个变量,对它模式匹配,执行对应的代码块。好像在哪儿听过?啊,就是函数定义时参数的模式匹配!好吧,模式匹配本质上不过就是case语句的语法糖而已。这两段代码就是完全等价的:
head' :: [a] -> ahead' [] = error "No head for empty lists!"
head' (x:_) = x head' :: [a] -> a
head' xs = case xs of [] -> error "No head for empty lists!"
(x:_) -> x
看得出,case表达式的语法十分简单:
case expression of pattern -> resultpattern -> result
pattern -> result
...
expression匹配合适的模式。如料,第一个模式若匹配,就执行第一个代码块;否则就交给下一个模式。如果到最后依然没有匹配的模式,就会产生一个运行时错误。
函数参数的模式匹配只能在定义函数时使用,而case表达式可以用在任何地方。例如:
describeList :: [a] -> StringdescribeList xs = "The list is " ++ case xs of [] -> "empty."
[x] -> "a singleton list."
xs -> "a longer list."
这在表达式中作模式匹配很方便,由于模式匹配本质上就是case表达式的语法糖,那么写成这样也是等价的:
describeList xs = "The list is " ++ what xs
where what [] = "empty."
what [x] = "a singleton list."
what xs = "a longer list."
递归
你好,递归!
前面的章节中我们简要谈了一下递归。而在本章,我们会深入地了解到它为何在haskell中是如此重要,能够以递归思想写出简洁优雅的代码。
如果你还不明白什么是递归,就读这个句子。哈哈!玩笑而已!递归实际上是定义函数以调用自身的方式。在数学定义中,递归随处可见,如斐波那契数列(fibonacci)。它先是定义两个非递归的数:F(0)=0,F(1)=1,表示斐波那契数列的前两个数为0和 1。然后就是对其他自然数,其斐波那契数就是它前面两个数字的和,即F(N)=F(N-1)+F(N-2)。这样一来,F(3)就是F(2)+F(1),进一步便是(F(1)+F(0))+F(1)。已经下探到了前面定义的非递归斐波那契数,可以放心地说F(3)就是2了。在递归定义中声明的一两个非递归的值(如F(0)和F(1))也可以称作边界条件,这对递归函数的正确求值至关重要。要是前面没有定义F(0)和F(1)的话,它下探到0之后就会进一步到负数,你就永远都得不到结果了。一不留神它就算到了F(-2000)=F(-2001)+F(-2002),并且永远都算不到头!
递归在haskell中至关重要。命令式语言要求你提供求解的步骤,haskell则倾向于让你提供问题的描述。这便是haskell没有while或for循环的原因,递归是我们的替代方案。
麦克西米不可思议
maximum函数取一组可排序的List(属于 Ord类型类)做参数,并返回其中的最大值。想想,在命令式风格中这一函数该怎么实现。很可能你会设一个变量来存储当前的最大值,然后用循环遍历该 List,若存在比这个值更大的元素,则修改变量为这一元素的值。到最后,变量的值就是运算结果。唔!描述如此简单的算法还颇费了点口舌呢!
现在看看递归的思路是如何:我们先定下一个边缘条件,即处理单个元素的List时,返回该元素。如果该List的头部大于尾部的最大值,我们就可以假定较长 的List的最大值就是它的头部。而尾部若存在比它更大的元素,它就是尾部的最大值。就这么简单!现在,我们在haskell中实现它
maximum' :: (Ord a) => [a] -> amaximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:xs)
| x > maxTail = x
| otherwise = maxTail
where maxTail = maximum' xs
如你所见,模式匹配与递归简直就是天造地设!大多数命令式语言中都没有模式匹配,于是你就得造一堆if-else来测试边界条件。而在这里,我们仅需要使用 模式将其表示出来。第一个模式说,如果该List为空,崩溃!就该这样,一个空List的最大值能是啥?我不知道。第二个模式也表示一个边缘条件,它说, 如果这个List仅包含单个元素,就返回该元素的值。
现在是第三个模式,执行动作的地方。 通过模式匹配,可以取得一个List的头部和尾部。这在使用递归处理List时是十分常见的。出于习惯,我们用个where语句来表示maxTail作为该List中尾部的最大值,然后检查头部是否大于尾部的最大值。若是,返回头部;若非,返回尾部的最大值。
我们取个List[2,5,1]做例子来看看它的工作原理。当调用maximum'处理它时,前两个模式不会被匹配,而第三个模式匹配了它并将其分为2与[5,1]。 where子句再取[5,1]的最大值。于是再次与第三个模式匹配,并将[5,1]分割为5和[1]。继续,where子句取[1]的最大值,这时终于到了边缘条件!返回1。进一步,将5与[1]中的最大值做比较,易得5,现在我们就得到了[5,1]的最大值。再进一步,将2与[5,1]中的最大值相比较,可得5更大,最终得5。
改用max函数会使代码更加清晰。如果你还记得,max函数取两个值做参数并返回其中较大的值。如下便是用max函数重写的maximun'
maximum' :: (Ord a) => [a] -> amaximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:xs) = max x (maximum' xs)
太漂亮了!一个List的最大值就是它的首个元素与它尾部中最大值相比较所得的结果,简明扼要。
几个递归函数
现在我们已经了解了递归的思路,接下来就使用递归来实现几个函数. 先实现下replicate函数, 它取一个Int值和一个元素做参数, 返回一个包含多个重复元素的List, 如replicate 3 5返回[5,5,5]. 考虑一下, 我觉得它的边界条件应该是负数. 如果要replicate重复某元素零次, 那就是空List. 负数也是同样, 不靠谱.
replicate' :: (Num i, Ord i) => i -> a -> [a]replicate' n x
| n <= 0 = []
| otherwise = x:replicate' (n-1) x
在这里我们使用了门卫而非模式匹配, 是因为这里做的是布尔判断. 如果n小于0就返回一个空List, 否则, 返回以x作首个元素并后接重复n-1次x的List. 最后, (n-1)的那部分就会令函数抵达边缘条件.
Note: Num不是Ord的子集, 表示数字不一定得拘泥于排序, 这就是在做加减法比较时要将Num与Ord类型约束区别开来的原因.
接下来实现take函数, 它可以从一个List取出一定数量的元素. 如take 3 [5,4,3,2,1],得[5,4,3]. 若要取零或负数个的话就会得到一个空List. 同样, 若是从一个空List中取值, 它会得到一个空List. 注意, 这儿有两个边界条件, 写出来:
take' :: (Num i, Ord i) => i -> [a] -> [a]take' n _
| n <= 0 = []
take' _ [] = []
take' n (x:xs) = x : take' (n-1) xs
首个模式辨认若为0或负数, 返回空List. 同时注意这里用了一个门卫却没有指定otherwise部分, 这就表示n若大于0, 会转入下一模式. 第二个模式指明了若试图从一个空List中取值, 则返回空List. 第三个模式将List分割为头部和尾部, 然后表明从一个list中取多个元素等同于令x作头部后接从尾部取n-1个元素所得的List. 假如我们要从[4,3,2,1]中取3个元素, 试着从纸上写出它的推导过程
reverse函数简单地反转一个List, 动脑筋想一下它的边界条件! 该怎样呢? 想想...是空List! 空List的反转结果还是它自己. Okay , 接下来该怎么办? 好的, 你猜的出来. 若将一个List分割为头部与尾部, 那它反转的结果就是反转后的尾部与头部相连所得的List.
reverse' :: [a] -> [a]reverse' [] = []
reverse' (x:xs) = reverse' xs ++ [x]
继续进发!
haskell支持无限List,所以我们的递归就不必添加边界条件。这样一来,它可以对某值计算个没完, 也可以产生一个无限的数据结构,如无限List。而无限List的好处就在于我们可以在任意位置将它断开.
repeat函数取一个元素作参数, 返回一个仅包含该元素的无限List. 它的递归实现简单的很, 看:
repeat' :: a -> [a]repeat' x = x:repeat' x
调用repeat 3会得到一个以3为头部并无限数量的3为尾部的List, 可以说repeat 3运行起来就是3:repeat 3, 然后3:3:3:3等等. 若执行repeat 3, 那它的运算永远都不会停止。而take 5 (repeat 3)就可以得到5个3, 与replicate 5 3差不多.
zip取两个List作参数并将其捆在一起。zip [1,2,3] [2,3]返回[(1,2),(2,3)], 它会把较长的List从中间断开, 以匹配较短的List. 用zip处理一个List与空List又会怎样? 嗯, 会得一个空List, 这便是我们的限制条件, 由于zip取两个参数, 所以要有两个边缘条件
zip' :: [a] -> [b] -> [(a,b)]zip' _ [] = []
zip' [] _ = []
zip' (x:xs) (y:ys) = (x,y):zip' xs ys
前两个模式表示两个List中若存在空List, 则返回空List. 第三个模式表示将两个List捆绑的行为, 即将其头部配对并后跟捆绑的尾部. 用zip处理[1,2,3]与['a','b']的话, 就会在[3]与[]时触及边界条件, 得到(1,'a'):(2,'b'):[]的结果,与[(1,'a'),(2,'b')]等价.
再实现一个标准库函数--elem! 它取一个元素与一个List作参数, 并检测该元素是否包含于此List. 而边缘条件就与大多数情况相同, 空List. 大家都知道空List中不包含任何元素, 便不必再做任何判断
elem' :: (Eq a) => a -> [a] -> Boolelem' a [] = False
elem' a (x:xs)
| a == x = True
| otherwise = a `elem'` xs
简单直接. 若头部不是该元素, 就检测尾部, 若为空List就返回False
排序,要快!
假定我们有一个可排序的List,其中元素的类型为Ord类型类的成员. 现在我们要给它排序! 有个排序算法非常的酷, 就是快速排序(quick sort), 睿智的排序方法. 尽管它在命令式语言中也不过10行, 但在haskell下边要更短,更漂亮, 俨然已经成了haskell的招牌了. 嗯, 我们在这里也实现一下. 或许会显得很俗气, 因为每个人都用它来展示haskell究竟有多优雅!
它的类型声明应为quicksort :: (Ord a) => [a] -> [a], 没啥奇怪的. 边界条件呢? 如料,空List。排过序的空List还是空List。接下来便是算法的定义:排过序的List就是令所有小于等于头部的元素在先(它们已经排过了序), 后跟大于头部的元素(它们同样已经拍过了序)。 注意定义中有两次排序,所以就得递归两次!同时也需要注意算法定义的动词为"是"什么而非"做"这个,"做"那个,再"做"那个...这便是函数式编程之美!如何才能从List中取得比头部小的那些元素呢?List Comprehension。好,动手写出这个函数!
quicksort :: (Ord a) => [a] -> [a]quicksort [] = []
quicksort (x:xs) =
let smallerSorted = quicksort [a | a <- xs, a <= x] biggerSorted = quicksort [a | a <- xs, a > x]
in smallerSorted ++ [x] ++ biggerSorted
小小的测试一下, 看看结果是否正确~
ghci> quicksort [10,2,5,3,1,6,7,4,2,3,4,8,9][1,2,2,3,3,4,4,5,6,7,8,9,10]
ghci> quicksort "the quick brown fox jumps over the lazy dog"
" abcdeeefghhijklmnoooopqrrsttuuvwxyz"
booyah! 如我所说的一样! 若给[5,1,9,4,6,7,3]排序,这个算法就会取出它的头部,即5。 将其至于分别比它大和比它小的两个List中间,得[1,4,3] ++ [5] ++ [9,6,7],我们便知道了当排序结束之时,5会在第四位,因为有3个数比它小每,也有三个数比它大。好的,接着排[1,4,3]与[9,6,7],结果就出来了!对它们的排序也是使用同样的函数,将它们分成许多小块,最终到达临界条件,即空List经排序依然为空,有个插图:
橙色的部分表示已定位并不再移动的元素。从左到右看,便是一个排过序的List。在这里我们将所有元素与head作比较,而实际上就快速排序算法而言,选择任意元素都是可以的。被选择的元素就被称作锚(pivot),以方便模式匹配。小于锚的元素都在浅绿的部分,大于锚都在深绿部分,这个黄黄的坡就表示了快速排序的执行方式:
递归地思考
我们已经递不少归了,也许你已经发觉了其中的固定模式:先定义一个边界条件,再定义个函数,让它从一堆元素中取一个并做点事情后,把余下的元素重新交给这个函数。 这一模式对List、Tree等数据结构都是适用的。例如,sum函数就是一个List头部与其尾部的sum的和。一个List的积便是该List的头与其尾部的积相乘的积,一个List的长度就是1与其尾部长度的和. 等等
再者就是边界条件。一般而言,边界条件就是为避免程序出错而设置的保护措施,处理List时的边界条件大部分都是空List,而处理Tree时的边界条件就是没有子元素的节点。
处理数字时也与之相似。函数一般都得接受一个值并修改它。早些时候我们编写过一个计算斐波纳契的函数,它便是某数与它减一的斐波纳契数的积。让它乘以零就不行了, 斐波纳契数又都是非负数,边界条件便可以定为1,即乘法的单位元。 因为任何数乘以1的结果还是这个数。而在sum中,加法的单位元就是0。在快速排序中,边界条件和单位元都是空List,因为任一List与空List相加的结果依然是原List。
高阶函数
haskell中的函数可以作为参数和返回值传来传去,这样的函数就被称作高阶函数。高阶函数可不只是某简单特性而已,它贯穿于haskell的方方面面。要拒绝循环与状态的改变而通过定义问题"是什么"来解决问题,高阶函数必不可少。它们是编码的得力工具。
柯里函数
本质上,haskell的所有函数都只有一个参数,那么我们先前编那么多含有多个参数的函数又是怎么回事? 呵,小伎俩! 所有多个参数的函数都是柯里函数。 什么意思呢? 取一个例子最好理解,就拿我们的好朋友max函数说事吧。它看起来像是取两个参数,返回较大的那个数。 实际上,执行max 4 5时,它会首先返回一个取一个参数的函数,其返回值不是4就是该参数,取决于谁大。 然后,以5为参数调用它,并取得最终结果。 这听着挺绕口的,不过这一概念十分的酷! 如下的两个调用是等价的:
ghci> max 4 55
ghci> (max 4) 5
5
把空格放到两个东西之间,称作函数调用。它有点像个运算符,并拥有最高的优先级。 看看max函数的类型:max :: (Ord a) => a -> a -> a。 也可以写作:max :: (Ord a) => a -> (a -> a)。 可以读作max取一个参数a,并返回一个函数(就是那个->),这个函数取一个a类型的参数,返回一个a。 这便是为何只用箭头来分隔参数和返回值类型。
这样的好处又是如何? 简言之,我们若以不全的参数来调用某函数,就可以得到一个不全调用的函数。 如果你高兴,构造新函数就可以如此便捷,将其传给另一个函数也是同样方便。
看下这个函数,简单至极:
multThree :: (Num a) => a -> a -> a -> amultThree x y z = x * y * z
我们若执行mulThree 3 5 9或((mulThree 3) 5) 9,它背后是如何运作呢? 首先,按照空格分隔,把3交给mulThree。 这返回一个返回函数的函数。 然后把5交给它,返回一个取一个参数并使之乘以15的函数。 最后把9交给这一函数,返回135。 想想,这个函数的类型也可以写作multThree :: (Num a) => a -> (a -> (a -> a)),->前面的东西就是函数取的参数,后面的东西就是其返回值。 所以说,我们的函数取一个a,并返回一个类型为(Num a) => a -> (a -> a)的函数,类似,这一函数返回一个取一个a,返回一个类型为(Num a) => a -> a的函数。 而最后的这个函数就只取一个a并返回一个a,如下:
ghci> let multTwoWithNine = multThree 9ghci> multTwoWithNine 2 3
54
ghci> let multWithEighteen = multTwoWithNine 2
ghci> multWithEighteen 10
180
前面提到,以不全的参数调用函数可以方便地创造新的函数。例如,搞个取一数与100比较大小的函数该如何? 大可这样:
compareWithHundred :: (Num a,Ord a) => a -> OrderingcompareWithHundred x = compare 100 x
用99调用它,就可以得到一个GT。 简单。 注意下在等号两边都有x。 想想compare 100会返回什么?一个取一数与100比较的函数。 Wow,这不正是我们想要的? 这样重写:
compareWithHundred :: (Num a,Ord a) => a -> OrderingcompareWithHundred = compare 100
类型声明依然相同,因为compare 100返回函数。 compare的类型为(Ord a) => a -> (a -> Ordering),用100调用它后返回的函数类型为(Num a,Ord a) => a -> Ordering,同时由于100还是Num类型类的实例,所以还得另留一个类约束。
Yo! 你得保证已经弄明白了柯里函数与不全调用的原理,它们很重要!
中缀函数也可以不全调用,用括号把它和一边的参数括在一起就行了。 这返回一个取一参数并将其补到缺少的那一端的函数。 一个简单函数如下:
divideByTen :: (Floating a) => a -> adivideByTen = (/10)
调用divideByTen 200就是(/10) 200,和200 / 10等价。
一个检查字符是否为大写的函数:
isUpperAlphanum :: Char -> BoolisUpperAlphanum = (`elem` ['A'..'Z'])
唯一的例外就是-运算符,按照前面提到的定义,(-4)理应返回一个并将参数减4的函数,而实际上,处于计算上的方便,(-4)表示负4。 若你一定要弄个将参数减4的函数,就用subtract好了,像这样(subtract 4).
若不用let给它命名或传到另一函数中,在ghci中直接执行multThree 3 4会怎样?
ghci> multThree 3 4:1:0:
No instance for (Show (t -> t))
arising from a use of `print' at :1:0-12
Possible fix: add an instance declaration for (Show (t -> t))
In the expression: print it
In a 'do' expression: print it
ghci说,这一表达式返回了一个a -> a类型的函数,但它不知道该如何显示它。 函数不是Show类型类的实例,所以我们不能得到表示一函数内容的字符串。 若在ghci中计算1+1,它会首先计算得2,然后调用show 2得到该数值的字符串表示,即"2",再输出到屏幕.
是时候了,来点高阶函数!
haskell中的函数可以取另一个函数做参数,也可以返回函数。 举个例子,我们弄个取一个函数并调用它两次的函数.
applyTwice :: (a -> a) -> a -> aapplyTwice f x = f (f x)
首先注意这类型声明。 在此之前我们很少用到括号,因为(->)是自然的右结合,不过在这里括号是必须的。 它标明了首个参数是个参数与返回值类型都是a的函数,第二个参数与返回值的类型也都是a。 我们可以用柯里函数的思路来理解这一函数,不过免得自寻烦恼,我们姑且直接把它看作是取两个参数返回一个值,其首个参数是个类型为(a->a)的函数,第二个参数是个a。 该函数的类型可以是(Int->Int),也可以是(String->String),但第二个参数必须与之一致。
Note: 现在开始我们会直说某函数含有多个参数(除非它真的只有一个参数)。 以简洁之名,我们会说(a->a->a)取两个参数,尽管我们知道它在背后做的手脚.
这个函数是相当的简单,就拿参数f当函数,用x调用它得到的结果再去调用它。 也就可以这样玩:
ghci> applyTwice (+3) 1016
ghci> applyTwice (++ " HAHA") "HEY"
"HEY HAHA HAHA"
ghci> applyTwice ("HAHA " ++) "HEY"
"HAHA HAHA HEY"
ghci> applyTwice (multThree 2 2) 9
144
ghci> applyTwice (3:) [1]
[3,3,1]
看,不全调用多神奇! 如果有个函数要我们给它传个一元函数,大可以不全调用一个函数让它剩一个参数,再把它交出去。
接下来我们用高阶函数的编程思想来实现个标准库中的函数,它就是zipWith。 它取一个函数和两个List做参数,并把两个List交到一起(使相应的元素去调用该函数)。 如下就是我们的实现:
zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c]zipWith' _ [] _ = []
zipWith' _ _ [] = []
zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys
看下这个类型声明,它的首个参数是个函数,取两个参数处理交叉,其类型不必相同,不过相同也没关系。 第二三个参数都是List,返回值也是个List。 第一个List中元素的类型必须是a,因为这个处理交叉的函数的第一个参数是a。 第二个List中元素的类型必为b,因为这个处理交叉的函数第二个参数的类型是b。 返回的List中元素类型为c。 如果一个函数说取一个类型为a->b->c的函数做参数,传给它个a->a->c类型的也是可以的,但反过来就不行了。 可以记下,若在使用高阶函数的时候不清楚其类型为何,就先忽略掉它的类型声明,再到ghci下用:t命令来看下haskell的类型推导.
这函数的行为与普通的zip很相似,边界条件也是相同,只不过多了个参数,即处理元素交叉的函数。它关不着边界条件什么事儿,所以我们就只留一个_ 。后一个模式的函数体与zip也很像,只不过这里是f x y而非(x,y)。 只要足够通用,一个简单的高阶函数可以在不同的场合反复使用。 如下便是我们zipWith'函数本领的冰山一角:
ghci> zipWith' (+) [4,2,5,6] [2,6,2,3][6,8,7,9]
ghci> zipWith' max [6,3,2,1] [7,3,1,5]
[7,3,2,5]
ghci> zipWith' (++) ["foo ","bar ","baz "] ["fighters","hoppers","aldrin"]
["foo fighters","bar hoppers","baz aldrin"]
ghci> zipWith' (*) (replicate 5 2) [1..]
[2,4,6,8,10]
ghci> zipWith' (zipWith' (*)) [[1,2,3],[3,5,6],[2,3,4]] [[3,2,2],[3,4,5],[5,4,3]]
[[3,4,6],[9,20,30],[10,12,12]]
如你所见,一个简单的高阶函数就可以玩出很多花样。 命令式语言使用for、while、赋值、状态检测来实现功能,再包起来留个接口,使之像个函数一样调用。而函数式语言使用高阶函数来抽象出常见的模式,像成对遍历并处理两个List或从中筛掉自己不需要的结果。
接下来实现标准库中的另一个函数flip,flip简单地取一个函数作参数并返回一个相似的函数,只是它们的两个参数倒了个。
flip' :: (a -> b -> c) -> (b -> a -> c)flip' f = g
where g x y = f y x
从这类型声明中可以看出,它取一个函数,其参数类型分别为a和b,而它返回的函数的参数类型为b和a。 由于函数默认都是柯里化的,->为右结合,这里的第二对括号其实并无必要,(a -> b -> c) -> (b -> a -> c)与(a -> b -> c) -> (b -> (a -> c))等价,也与(a -> b -> c) -> b -> a -> c等价。 前面我们写了g x y = f y x,既然这样可行,那么f y x = g x y不也一样? 这一来我们可以改成更简单的写法:
flip' :: (a -> b -> c) -> b -> a -> cflip' f y x = f x y
在这里我们就利用了柯里函数的优势,只要调用flip' f而不带y和x,它就会返回一个俩参数倒个的函数。flip处理的函数往往都是用来传给其他函数调用,于是我们可以发挥柯里函数的优势,预先想好发生完全调用的情景并处理好返回值.
ghci> flip' zip [1,2,3,4,5] "hello"[('h',1),('e',2),('l',3),('l',4),('o',5)]
ghci> zipWith (flip' div) [2,2..] [10,8,6,4,2]
[5,4,3,2,1]
map 与 filter
map取一个函数和List做参数,遍历该List的每个元素来调用该函数产生一个新的List。 看下它的类型声明和实现:
map :: (a -> b) -> [a] -> [b]map _ [] = []
map f (x:xs) = f x : map f xs
从这类型声明中可以看出,它取一个取a返回b的函数和一组a的List,并返回一组b。 这就是haskell的有趣之处:有时只看类型声明就能对函数的行为猜个大致。map函数多才多艺,有一百万种用法。 如下是其中一小部分:
ghci> map (+3) [1,5,3,1,6][4,8,6,4,9]
ghci> map (++ "!") ["BIFF","BANG","POW"]
["BIFF!","BANG!","POW!"]
ghci> map (replicate 3) [3..6]
[[3,3,3],[4,4,4],[5,5,5],[6,6,6]]
ghci> map (map (^2)) [[1,2],[3,4,5,6],[7,8]]
[[1,4],[9,16,25,36],[49,64]]
ghci> map fst [(1,2),(3,5),(6,3),(2,6),(2,5)]
[1,3,6,2,2]
你可能会发现,以上的所有代码都可以用List Comprehension来替代。map (+3) [1,5,3,1,6]与[x+3 | x <- [1,5,3,1,6]完全等价。
filter函数取一个限制条件和一个List,返回该List中所有符合该条件的元素。 它的类型声明及实现大致如下:
filter :: (a -> Bool) -> [a] -> [a]filter _ [] = []
filter p (x:xs)
| p x = x : filter p xs
| otherwise = filter p xs
很简单。 只要p x所得的结果为真,就将这一元素加入新List,否则就无视之。几个使用范例:
ghci> filter (>3) [1,5,3,2,1,6,4,3,2,1][5,6,4]
ghci> filter (==3) [1,2,3,4,5]
[3]
ghci> filter even [1..10]
[2,4,6,8,10]
ghci> let notNull x = not (null x) in filter notNull [[1,2,3],[],[3,4,5],[2,2],[],[],[]]
[[1,2,3],[3,4,5],[2,2]]
ghci> filter (`elem` ['a'..'z']) "u LaUgH aT mE BeCaUsE I aM diFfeRent"
"uagameasadifeent"
ghci> filter (`elem` ['A'..'Z']) "i lauGh At You BecAuse u r aLL the Same"
"GAYBALLS"
同样,以上都可以用List Comprehension的限制条件来实现。 并没有教条规定你必须在什么情况下用map和filter还是List Comprehension,选择权归你,看谁舒服用谁就是。 如果有多个限制条件,只能连着套好几个filter或用&&等逻辑函数的组合之,这时就不如list comprehension来得爽了。
还记得上一章的那个quicksort函数么? 我们用到了List Comprehension来过滤大于或小于锚的元素。 换做filter也可以实现,而且更加易读:
quicksort :: (Ord a) => [a] -> [a]quicksort [] = []
quicksort (x:xs) =
let smallerSorted = quicksort (filter (x) xs)
in smallerSorted ++ [x] ++ biggerSorted
map和filter是每个函数式程序员的面包黄油(呃,map和filter还是List Comprehension并不重要)。 想想前面我们如何解决给定周长寻找合适直角三角形的问题的? 在命令式编程中,我们可以套上三个循环逐个测试当前的组合是否满足条件,若满足,就打印到屏幕或其他类似的输出。 而在函数式编程中,这行就都交给map和filter。 你弄个取一参数的函数,把它交给map过一遍List,再filter之找到合适的结果。 感谢haskell的惰性,即便是你多次map一个list也只会遍历一遍该list,要找出小于100000的数中最大的3829的倍数,只需过滤结果所在的list就行了.
要找出小于100000的3829的所有倍数,我们应当过滤一个已知结果所在的list.
largestDivisible :: (Integral a) => alargestDivisible = head (filter p [100000,99999..])
where p x = x `mod` 3829 == 0
首先, 取一个降序的小于100000所有数的List,然后按照限制条件过滤它。 由于这个List是降序的,所以结果List中的首个元素就是最大的那个数。 惰性再次行动! 由于我们只取这结果List的首个元素,所以它并不关心这List是有限还是无限的,在找到首个合适的结果处运算就停止了。
接下来,我们就要找出所有小于10000的奇数的平方和,得先提下takeWhile函数,它取一个限制条件和List作参数,然后从头开始遍历这一List,并返回符合限制条件的元素。 而一旦遇到不符合条件的元素,它就停止了。 如果我们要取出字符串"elephants know how to party"中的首个单词,可以takeWhile (/=' ') "elephants know how to party",返回"elephants"。 okay,要求所有小于10000的奇数的平方的和,首先就用(^2)函数map掉这个无限的List[1..]。然后过滤之,只取奇数就是了。 在大于10000处将它断开,最后前面的所有元素加到一起。 这一切连写函数都不用,在ghci下直接搞定.
ghci> sum (takeWhile (<10000) (filter odd (map (^2) [1..])))166650
不错! 先从几个初始数据(表示所有自然数的无限list),再map它,filter它,切它,直到它符合我们的要求,再将其加起来。 这用list comprehension也是可以的,而哪种方式就全看你的个人口味.
ghci> sum (takeWhile (<10000) [m | m <- [n^2 | n <- [1..]], odd m])166650
感谢haskell的惰性特质,这一切才得以实现。 我们之所以可以map或filter一个无限list,是因为它的操作不会被立即执行,而是拖延一下。 只有我们要求haskell交给我们sum的结果的时候,sum函数才会跟takeWhile说,它要这些数。takeWhile就再去要求filter和map行动起来,并在遇到大于等于10000时候停止.
下个问题与Collatz序列有关,取一个自然数,若为偶数就除以2。 若为奇数就乘以3再加1。 再用相同的方式处理所得的结果,得到一组数字构成的的链。 它有个性质,无论任何以任何数字开始,最终的结果都会归1。 所以若拿13当作起始数,就可以得到这样一个序列13,40,20,10,5,16,8,4,2,1。13*3+1得40,40除2得20,如是继续,得到一个10个元素的链。
好的,我们想知道的是: 以1到100之间的所有数作为起始数,会有多少个链的长度大于15?
chain :: (Integral a) => a -> [a]chain 1 = [1]
chain n
| even n = n:chain (n `div` 2)
| odd n = n:chain (n*3 + 1)
该链止于1,这便是边界条件。 标准的递归函数:
ghci> chain 10[10,5,16,8,4,2,1]
ghci> chain 1
[1]
ghci> chain 30
[30,15,46,23,70,35,106,53,160,80,40,20,10,5,16,8,4,2,1]
yay! 貌似工作良好。 现在由这个函数来告诉我们结果:
numLongChains :: IntnumLongChains = length (filter isLong (map chain [1..100]))
where isLong xs = length xs > 15
我们把chain函数map到[1..100],得到一组链的list,然后用个限制条件过滤长度大于15的链。 过滤完毕后就可以得出结果list中的元素个数.
Note: 这函数的类型为numLongChains :: Int。 这是由于历史原因,length返回一个Int而非Num的成员类型,若要得到一个更通用的Num a,我们可以使用fromInterval函数来处理所得结果.
用map,我们可以写出类似map (*) [0..]之类的代码。 如果只是为了例证柯里函数和不全调用的函数是真正的值及其原理,那就是你可以把函数传递或把函数装在list中(只是你还不能将它们转换为字符串)。 迄今为止,我们还只是map单参数的函数到list,如map (*2) [0..]可得一组类型为(Num a) => [a]的list,而map (*) [0..]也是完全没问题的。*的类型为(Num a) -> a -> a -> a,用单个参数调用二元函数会返回一个一元函数。 如果用*来map 一个[0..]的list,就会得到一组一元函数组成的list,即(Num a) => [a->a]。map (*) [0..]所得的结果写起来大约就是[(*0),(*1),(*2)..]
ghci> let listOfFuns = map (*) [0..]ghci> (listOfFuns !! 4) 5
20
取所得list的第四个元素可得一函数,与(*4)等价。 然后用5调用它,与(* 4) 5或4*5都是等价的.
lambda
lambda就是匿名函数。有些时候我们需要传给高阶函数一个函数,而这函数我们只会用这一次,这就弄个特定功能的lambda。编写lambda,就写个/(因为它看起来像是希腊字母的lambda--如果你斜视的厉害),后面是用空格分隔的参数,->后面就是函数体。通常我们都是用括号将其括起,要不然它就会占据整个右边部分。
向上5英寸左右,你会看到我们在numLongChain函数中用where语句声明了个isLong函数传递给了filter。好的,用lambda代替它。
numLongChains :: IntnumLongChains = length (filter (/xs -> length xs > 15) (map chain [1..100]))
lambda是个表达式,因此我们可以任意传递。表达式(/xs -> length xs > 15)返回一个函数,它可以告诉我们一个list的长度是否大于15。
不熟悉柯里函数与不全调用的人们往往会写出很多lambda,而实际上大部分都是没必要的。例如,表达式map (+3) [1,6,3,2]与map (/x -> x+3) [1,6,3,2]等价,(+3)和(/x -> x+3)都是给一个数加上3。不用说,在这种情况下不用lambda要清爽的多。
和普通函数一样,lambda也可以取多个参数。
ghci> zipWith (/a b -> (a * 30 + 3) / b) [5,4,3,2,1] [1,2,3,4,5][153.0,61.5,31.0,15.75,6.6]
同普通函数一样,你也可以在lambda中使用模式匹配,只是你无法为一个参数设置多个模式,如[]和(x:xs)。lambda的模式匹配若失败,就会引发一个运行时错误,所以慎用!
ghci> map (/(a,b) -> a + b) [(1,2),(3,5),(6,3),(2,6),(2,5)][3,8,9,8,7]
一般情况下,lambda都是括在括号中,除非我们想要后面的整个语句都作为lambda的函数体。很有趣,由于有柯里化,如下的两段是等价的:
addThree :: (Num a) => a -> a -> a -> aaddThree x y z = x + y + zaddThree :: (Num a) => a -> a -> a -> a
addThree = /x -> /y -> /z -> x + y + z
这样的函数声明与函数体中都有->,这一来类型声明的写法就很明白了。当然第一段代码更易读,不过第二个函数使得柯里化更容易理解。
有些时候用这种语句写还是挺酷的,我觉得这应该是最易读的flip函数实现了:
flip' :: (a -> b -> c) -> b -> a -> cflip' f = /x y -> f y x
尽管这与flip' f x y = f y x等价,但它可以更明白地表示出它会产生一个新的函数。flip常用来处理一个函数,再将返回的新函数传递给map或filter。所以如此使用lambda可以更明确地表现出返回值是个函数,可以用来传递给其他函数作参数。
折叠纸鹤
回到当初我们学习递归的情景。我们会发现处理list的许多函数都有固定的模式,通常我们会将边界条件设置为空list,再引入(x:xs)模式,对单个元素和余下的list做些事情。这一模式是如此常见,因此haskell引入了一组函数来使之简化,也就是fold。它们与map有点像,只是它们返回的是单个值。
一个fold取一个二元函数,一个初始值(我喜欢管它叫累加值)和一个需要fold(折叠)的list。这个二元函数有两个参数,即累加值和list的首项(或尾项),返回值是新的累加值。然后,以新的累加值和新的list首项调用该函数,如是继续。到list遍历完毕时,只剩下一个累加值,也就是最终的结果。
首先看下foldl函数,也叫做左折叠。它从list的左端开始折叠,用初始值和list的头部调用这二元函数,得一新的累加值,并用新的累加值与list的下一个元素调用二元函数。如是继续。
我们再实现下sum,这次用fold替代那复杂的递归:
sum' :: (Num a) => [a] -> asum' xs = foldl (/acc x -> acc + x) 0 xs
测试下,一二三~
ghci> sum' [3,5,2,1]11
我们深入看下fold的执行过程:/acc x-> acc + x是个二元函数,0是初始值,xs是待折叠的list。一开始,累加值为0,当前项为3,调用二元函数0+3得3,作新的累加值。接着来,累加值为3,当前项为5,得新累加值8。再往后,累加值为8,当前项为2,得新累加值10。最后累加值为10,当前项为1,得11。恭喜,你完成了一次折叠(fold)!
左边的这个图表示了折叠的执行过程,一步又一步(一天又一天!)。浅棕色的数字都是累加值,你可以从中看出list是如何从左端一点点加到累加值上的。唔对对对!如果我们考虑到函数的柯里化,可以写出更简单的实现:
sum' :: (Num a) => [a] -> asum' = foldl (+) 0
这个lambda函数(/acc x -> acc + x )与(+)等价。我们可以把xs等一应参数省略掉,反正调用foldl (+) 0会返回一个取list作参数的函数。通常,如果你的函数类似foo a = bar b a, 大可改为foo = bar b。有柯里化嘛。
呼呼,进入右折叠前我们再实现个用到左折叠的函数。大家肯定都知道elem是检查某元素是否属于某list的函数吧,我就不再提了(唔,刚提了)。用左折叠实现它:
elem' :: (Eq a) => a -> [a] -> Boolelem' y ys = foldl (/acc x -> if x == y then True else acc) False ys
好好好,这里我们有什么?起始值与累加值都是布尔值。在处理fold时,累加值与最终结果的类型总是相同的。如果你不知道怎样对待起始值,那我告诉你,我们先假设它不存在,以False开始。我们要是fold一个空list,结果就是False。然后我们检查当前元素是否为我们寻找的,如果是,就令累加值为True,如果否,就保留原值不变。若False,及表明当前元素不是。若True,就表明已经找到了。
右折叠foldr的行为与左折叠相似,只是累加值是从list的右边开始。同样,左折叠的二元函数取累加值作首个参数,当前值为第二个参数(即/acc x -> ...),而右折叠的二元函数参数的顺序正好相反(即/x acc -> ...)。这倒也正常,毕竟是从右端开始折叠。
累加值可以是任何类型,可以是数值,布尔值,甚至一个新的list。我们可以用右fold实现map函数,累加值就是个list。将map处理过的元素一个一个连到一起。很容易想到,起始值就是空list。
map' :: (a -> b) -> [a] -> [b]map' f xs = foldr (/x acc -> f x : acc) [] xs
如果我们用(+3)来映射[1,2,3],它就会先到达list的右端,我们取最后那个元素,也就是3来调用(+3),得6。追加(:)到累加值上,6:[]得[6]并成为新的累加值。用2调用(+3),得5,追加到累加值,于是累加值成了[5,6]。再对1调用(+3),并将结果4追加到累加值,最终得结果[4,5,6]。
当然,我们也完全可以用左折叠来实现它,map' f xs = foldl (/acc x -> acc ++ [f x]) [] xs就行了。不过问题是,使用(++)往list后面追加元素的效率要比使用(:)低得多。所以在生成新list的时候人们一般都是使用右折叠。
反转一个list,既也可以通过右折叠,也可以通过左折叠。有时甚至不需要管它们的分别,如sum函数的左右折叠实现都是十分相似。不过有个大的不同,那就是右折叠可以处理无限长度的数据结构,而左折叠不可以。将无限list从中断开执行左折叠是可以的,不过若是向右,就永远到不了头了。
所有遍历list中元素并据此返回一个值的操作都可以交给fold实现。无论何时需要遍历list并返回某值,都可以尝试下fold。因此,fold的地位可以说与map和filter并驾齐驱,同为函数式编程中最常用的函数之一。
foldl1与foldr1的行为与foldl和foldr相似,只是你无需明确提供初始值。他们假定list的首个(或末尾)元素作为起始值,并从旁边的元素开始折叠。这一来,sum函数大可这样实现:sum = foldl1 (+)。这里待折叠的list中至少要有一个元素,若使用空list就会产生一个运行时错误。不过foldl和foldr与空list相处的就很好。所以在使用fold前,应该先想下它会不会遇到空list,如果不会遇到,大可放心使用foldr1和foldl1。
为了体会fold的威力,我们就用它实现几个库函数:
maximum' :: (Ord a) => [a] -> amaximum' = foldr1 (/x acc -> if x > acc then x else acc)
reverse' :: [a] -> [a]
reverse' = foldl (/acc x -> x : acc) []
product' :: (Num a) => [a] -> a
product' = foldr1 (*)
filter' :: (a -> Bool) -> [a] -> [a]
filter' p = foldr (/x acc -> if p x then x : acc else acc) []
head' :: [a] -> a
head' = foldr1 (/x _ -> x)
last' :: [a] -> a
last' = foldl1 (/_ x -> x)
仅靠模式匹配就可以实现head函数和last函数,而且效率也很高。这里只是为了演示,用fold的实现方法。我觉得我们这个reverse'定义的相当聪明,用一个空list做初始值,并向左展开list,从左追加到累加值,最后得到一个反转的新list。/acc x -> x : acc有点像:函数,只是参数顺序相反。所以我们可以改成foldl (flip (:)) []。
有个理解折叠的思路:假设我们有个二元函数f,起始值z,如果从右折叠[3,4,5,6],实际上执行的就是f 3 (f 4 (f 5 (f 6 z)))。f会被list的尾项和累加值调用,所得的结果会作为新的累加值传入下一个调用。假设f是(+),起始值z是0,那么就是3 + (4 + (5 + (6 + 0))),或等价的前缀形式:(+) 3 ((+) 4 ((+) 5 ((+) 6 0)))。相似,左折叠一个list,以g为二元函数,z为累加值,它就与g (g (g (g z 3) 4) 5) 6等价。如果用flip (:)作二元函数,[]为累加值(看得出,我们是要反转一个list),这就与flip (:) (flip (:) (flip (:) (flip (:) [] 3) 4) 5) 6等价。显而易见,执行该表达式的结果为[6,5,4,3]。
scanl和scanr与foldl和foldr相似,只是它们会记录下累加值的所有状态到一个list。也有scanl1和scanr1。
ghci> scanl (+) 0 [3,5,2,1][0,3,8,10,11]
ghci> scanr (+) 0 [3,5,2,1]
[11,8,3,1,0]
ghci> scanl1 (/acc x -> if x > acc then x else acc) [3,4,5,3,7,9,2,1]
[3,4,5,5,7,9,9,9]
ghci> scanl (flip (:)) [] [3,2,1]
[[],[3],[2,3],[1,2,3]]
当使用scanl时,最终结果就是list的最后一个元素。而在scanr中则是第一个。
sqrtSums :: IntsqrtSums = length (takeWhile (<1000) (scanl1 (+) (map sqrt [1..]))) + 1ghci> sqrtSums
131
ghci> sum (map sqrt [1..131])
1005.0942035344083
ghci> sum (map sqrt [1..130])
993.6486803921487
scan可以用来跟踪fold函数的执行过程。想想这个问题,取所有自然数的平方根的和,寻找在何处超过1000?先map sqrt [1..],然后用个fold来求它们的和。但在这里我们想知道求和的过程,所以使用scan,scan完毕时就可以得到小于1000的所有和。所得结果list的第一个元素为1,第二个就是1+根2,第三个就是1+根2+根3。若有x个和小于1000,那结果就是x+1。
有$的函数调用
好的,接下来看看$函数。它也叫作函数调用符。先看下它的定义:
($) :: (a -> b) -> a -> bf $ x = f x
什么鬼东西?这没啥意义的操作符?它只是个函数调用符罢了?好吧,不全是,但差不多。普通的函数调用符有最高的优先级,而$的优先级则最低。用空格的函数调用符是左结合的,如f a b c与((f a) b) c等价,而$则是右结合的。
听着不错。但有什么用?它可以减少我们代码中括号的数目。试想有这个表达式:sum (map sqrt [1..130])。由于低优先级的$,我们可以将其改为sum $ map sqrt [1..130],可以省敲不少键!sqrt 3 + 4 + 9会怎样?这会得到9,4和根3的和。若要取(3+4+9)的平方根,就得sqrt (3+4+9)或用$:sqrt $ 3+4+9。因为$有最低的优先级,所以你可以把$看作是在右面写一对括号的等价形式。
sum (filter (> 10) (map (*2) [2..10]))该如何?嗯,$是右结合,f (g (z x))与f $ g $ z x等价。所以我么可以将sum (filter (> 10) (map (*2) [2..10])重写为sum $ filter (> 10) $ map (*2) [2..10]。
除了减少括号外,$还可以将数据作为函数使用。例如映射一个函数调用符到一组函数组成的list:
ghci> map ($ 3) [(4+),(10*),(^2),sqrt][7.0,30.0,9.0,1.7320508075688772]
函数组合
在数学中,函数组合是这样定义的:,表示组合两个函数成为一个函数。以x调用这一函数,就与用x调用g再用所得的结果调用f等价。
haskell中的函数组合与之很像,即.函数。其定义为:
(.) :: (b -> c) -> (a -> b) -> a -> cf . g = /x -> f (g x)
注意下这类型声明,f的参数类型必须与g的返回类型相同。所以得到的组合函数的参数类型与g相同,返回类型与f相同。表达式negate . (*3)返回一个求一数字乘以3后的负数的函数。
函数组合的用处之一就是生成新函数,并传递给其它函数。当然我们可以用lambda实现,但大多数情况下,使用函数组合无疑更直白。假设我们有一组由数字组成的list,要将其全部转为负数,很容易就想到应先取其绝对值,再取负数,像这样:
ghci> map (/x -> negate (abs x)) [5,-3,-6,7,-3,2,-19,24][-5,-3,-6,-7,-3,-2,-19,-24]
注意下这个lambda与那函数组合是多么的相像。用函数组合,我们可以将代码改为:
ghci> map (negate . abs) [5,-3,-6,7,-3,2,-19,24][-5,-3,-6,-7,-3,-2,-19,-24]
漂亮!函数组合是右结合的,我们同时组合多个函数。表达式f (g (z x))与(f . g . z) x等价。按照这个思路,我们可以将
ghci> map (/xs -> negate (sum (tail xs))) [[1..5],[3..6],[1..7]][-14,-15,-27]
改为:
ghci> map (negate . sum . tail) [[1..5],[3..6],[1..7]][-14,-15,-27]
不过含多个参数的函数该怎么办?好,我们可以使用不全调用使每个函数都只剩下一个参数。sum (replicate 5 (max 6.7 8.9))可以重写为(sum . replicate 5 . max 6.7) 8.9或sum . replicate 5 . max 6.7 $ 8.9。在这里会产生一个函数,它取与max 6.7同样的参数,并使用结果调用replicate 5再用sum求和。最后用8.9调用该函数。不过一般你可以这么读,用8.9调用max 6.7,然后使它replicate 5,再sum之。如果你打算用函数组合来替掉那堆括号,可以先在最靠近参数的函数后面加一个$,接着就用.组合其所有函数调用,而不用管最后那个参数。如果有这样一段代码:replicate 100 (product (map (*3) (zipWith max [1,2,3,4,5] [4,5,6,7,8]))),可以改为:replicate 100 . product . map (*3) . zipWith max [1,2,3,4,5] $ [4,5,6,7,8]。如果表达式以3个括号结尾,就表示你可以将其修改为函数组合的形式。
函数组合的另一用途就是定义point free style(也称作pointless style)的函数。就拿我们之前写的函数作例子:
sum' :: (Num a) => [a] -> asum' xs = foldl (+) 0 xs
等号的两端都有个xs。由于有柯里化(Currying),我们可以省掉两端的xs。foldl (+) 0返回的就是一个取一list作参数的函数,我们把它修改为sum' = foldl (+) 0,这就是point free style。下面这个函数又该如何改成point free style呢?
fn x = ceiling (negate (tan (cos (max 50 x))))像刚才那样简单去掉两端的x是不行的,函数体中x的右边还有括号。cos (max 50)是有错误的,你不能求一个函数的余弦。我们的解决方法就是,使用函数组合。
fn = ceiling . negate . tan . cos . max 50漂亮!point free style会令你去思考函数的组合方式,而非数据的传递方式,更加简洁直白。你可以将一组简单的函数组合在一起,使之形成一个复杂的函数。不过函数若过于复杂,再使用point free style往往会适得其反,因此构造较长的函数组合链是不被鼓励的(虽然我本人热衷于函数组合)。更好的解决方法,就是使用let语句给中间的运算结果绑定一个名字,或者说把问题分解成几个小问题再组合到一起。这样一来我们代码的读者就可以轻松些,不必要纠结那巨长的函数组合链了。
在map和filter那节中,我们求了小于10000的所有奇数的平方的和。如下就是将其置于一个函数中的样子:
oddSquareSum :: IntegeroddSquareSum = sum (takeWhile (<10000) (filter odd (map (^2) [1..])))
身为函数组合狂人,我可能会这么写:
oddSquareSum :: IntegeroddSquareSum = sum . takeWhile (<10000) . filter odd . map (^2) $ [1..]
不过若是给别人看,我可能就这么写了:
oddSquareSum :: IntegeroddSquareSum =
let oddSquares = filter odd $ map (^2) [1..]
belowLimit = takeWhile (<10000) oddSquares
in sum belowLimit
这段代码可赢不了代码花样大赛,不过我们的读者可能会觉得它比函数组合链更好看。