06 高阶函数

所谓高阶函数,就是将函数对象作为函数的参数或者函数的返回值,高阶函数是抽象必不可少的工具

柯里化和部分函数

函数其实只接受一个参数

如果我说:Haskell 中,函数本质上只接受一个参数。你肯定会问:多参数函数怎么办呢?这时候高阶函数就出场了!

对于多参数函数,接受一个参数后,会返回一个接受剩余参数的函数,这就是柯里化。

比如常见的 max 函数,其类型

max :: (Ord a) => a -> a -> a

相当于,接受一个参数,返回另外一个接受一个参数的函数

max :: (Ord a) => a -> (a -> a)

ghci> max 4 5
5

相当于

ghci> (max 4) 5
5

代码中 (max 4) 生成了一个接受一个参数的部分函数。

部分参数生成部分函数

我们如果用部分参数调用函数,就会得到一个部分函数,所谓部分函数,就是指这个函数可以接受剩余的参数

注意:已经接受的参数作为状态被保存在了返回的部分函数中

--定义一个接收三个参数的函数
multThree :: (Num a) => a -> a -> a -> a
multThree x y z = x * y * z

--传入一个参数,返回一个接收两个参数的函数
ghci> let multTwoWithNine = multThree 9
ghci> multTwoWithNine 2 3
54

--继续传入一个参数,返回一个接受一个参数的函数
ghci> let multWithEighteen = multTwoWithNine 2
ghci> multWithEighteen 10
180

消除等号两边相同的变量可以得到部分函数

比如要定义一个和 100 比较大小的函数,传统方式是这样:

compareWithHundred :: (Num a, Ord a) => a -> Ordering
compareWithHundred x = compare 100 x

注意到定义两边都有变量 x,就像数学中等式变换一样,可以将等号两边的 x 消除,这样就得到了一个部分函数。

这个函数和上面函数的类型是完全相同的

compareWithHundred :: (Num a, Ord a) => a -> Ordering
compareWithHundred = compare 100

中缀函数生成部分函数

中缀函数生成部分函数需要用括号括起来,比如

divideByTen :: (Floating a) => a -> a
divideByTen = (/10)

调用 divideByTen 200 就是 (/10) 200,和 200 / 10 等价。

参数的位置

因为 Haskell 是静态类型的,因此参数的位置一般都是很明确的,不需要考虑太多

比如,检查字符是否为大写的函数可以简化为

isUpperAlphanum :: Char -> Bool
isUpperAlphanum = (`elem` ['A'..'Z'])

- 是个例外

这里减号 - 是个例外,因为他和负号相同,因此需要使用函数名 subtract

柯里化生成的部分函数不是 Show 的成员

和普通函数不同,通过部分调用生成的部分函数不能在交互式环境中显示,因为他不是 Show 的成员

高阶函数

首先看看以函数作为参数的高阶函数,比如接受一个函数作为参数并调用它两次

applyTwice :: (a -> a) -> a -> a  
applyTwice f x = f (f x)

注意看它的类型声明,第一个参数是一个函数类型

因为柯里化可以非常容易的创建函数,因此部分函数和高阶函数很容易结合使用,非常强大

ghci> applyTwice (+3) 10  
16  

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 函数

前面我们用过 zip 函数,其将两个列表中的元素组合为序对。

下面我们来实现一个 zipWith 高阶函数,他也是将两个列表中的元素经过一定的处理后,合并成一个列表,但处理方法是作为参数传进去的

zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c]  
--任意一个列表为空则返回空
zipWith' _ [] _ = []  
zipWith' _ _ [] = []  

zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys

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]]

flip 函数

flip 函数接受一个函数作参数,并回传一个相似的函数,只是将两个参数的顺序倒了个

下面的代码就是将这个问题描述了一遍:

  • 定义了一个 g 函数,其返回值就是用颠倒的顺序调用 f 函数
  • 最后将 g 函数返回
flip' :: (a -> b -> c) -> (b -> a -> c)  
flip' f = g  
    where g x y = f y x

数学等式又来了:消除中间变量 g 函数,可以进一步简化代码

再加上参数定义,这个函数就可以直接使用了

flip' :: (a -> b -> c) -> b -> a -> c  
flip' f y x = f x y

调用一下

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 函数就是列表变换

map :: (a -> b) -> [a] -> [b]  
map _ [] = []  
map f (x:xs) = f x : map f xs

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]

map 的大部分用法都可以用列表解析来代替

filter 函数,过滤列表

filter :: (a -> Bool) -> [a] -> [a]  
filter _ [] = []  
filter p (x:xs)   
    | p x       = x : filter p xs  
    | otherwise = filter p xs

使用方法

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"

注意:filter 的功能也可以用列表解析实现,特别是多条件过滤的情况下,用 filter 就比较啰嗦了

之前的快速排序使用列表解析进行过滤的,这里把他改成用 filter 过滤,代码如下

quicksort :: (Ord a) => [a] -> [a]    
quicksort [] = []    
quicksort (x:xs) =     
    let smallerSorted = quicksort (filter (<=x) xs)
        biggerSorted = quicksort (filter (>x) xs)   
    in  smallerSorted ++ [x] ++ biggerSorted

takeWhile 函数

从一个列表中取出符合条件的项,组成一个新列表。比如“找出所有小于 10000 且为奇数的平方数的和”

ghci> sum (takeWhile (<10000) (filter odd (map (^2) [1..])))  
166650

当然,也可以用列表解析实现

ghci> sum (takeWhile (<10000) [m | m <- [n^2 | n <- [1..]], odd m])  
166650

Collatz 串行

算法:

  • 取一个自然数,若为偶数就除以 2
    ;若为奇数就乘以 3 再加 1
  • 再用相同的方式处理所得的结果,循环得到一组数字构成的的链
  • 无论任何以任何数字开始,最终的结果都会归 1

问题:以 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)

调用效果如下

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]

下面是过滤函数

--生成一个列表,里面的项都是长度大于 15 的串链
numLongChains :: Int  
numLongChains = length (filter isLong (map chain [1..100]))  
    where isLong xs = length xs > 15

map 的其他用法

一般情况下使用 map ,列表中的项作为参数,正好符合处理函数需要,处理后的数据组成了新的列表,比如

map (*2) [0..]

有的时候,仅仅列表中的项作为参数,比处理函数需要的参数数目要少,这样得到的就不是转换后数据的列表,而是部分函数的列表。比如: map (*) [0..] 得到的结果为 [(0*),(1*),(2*)..]

ghci> let listOfFuns = map (*) [0..]  
ghci> (listOfFuns !! 4) 5  
20

lambda

lambda 就是匿名函数,可以省略函数名称的定义,例如前面过滤 Collatz 串的函数,其中的过滤条件函数可以用匿名函数替代

numLongChains :: Int  
numLongChains = length (filter (\xs -> length xs > 15) (map chain [1..100]))

用部分函数代替匿名函数

看看下面的代码,等式两边都有变量 x,我们自然而然想到了柯里化。

大部分用到匿名函数的地方都可以用部分函数代替,代码更简洁

map (\x -> x+3) [1,6,3,2]

//替换成

map (+3) [1,6,3,2]

参数相关问题

匿名函数也可以有多个参数

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]

也可以使用模式匹配,但是无法使用多种模式

ghci> map (\(a,b) -> a + b) [(1,2),(3,5),(6,3),(2,6),(2,5)]  
[3,8,9,8,7]

结合柯里化和匿名函数理解函数的定义

  1. 函数的定义本质上可以看做是给一个匿名函数对象起一个名字
  2. 由于柯里化,匿名函数也可以看做是只接受一个参数的函数

下面两段代码是等价的

addThree :: (Num a) => a -> a -> a -> a  
addThree x y z = x + y + z

addThree :: (Num a) => a -> a -> a -> a  
addThree = \x -> \y -> \z -> x + y + z

利用匿名函数让 flip 函数根据可读性

flip' :: (a -> b -> c) -> b -> a -> c  
flip' f = \x y -> f y x

折叠

折叠函数接受三个参数

  • 一个归约函数,接受两个参数:归约值和列表的当前项
  • 归约初值
  • 一个列表

lfold 函数:左折叠

用左折叠函数定义一个求和函数

sum' :: (Num a) => [a] -> a  
sum' xs = foldl (\acc x -> acc + x) 0 xs

注意:左折叠的规约函数的参数顺序是:归约值,列表的当前项

前面说过,大部分匿名函数都可以用部分函数代替,改写代码如下,非常的简洁

sum' :: (Num a) => [a] -> a  
sum' = foldl (+) 0

再用左折叠实现一个 elem 函数,这里想演示的是,初值怎么选取

注意:这里的实现效率不高,因为他在找到了目标元素后,仍然继续遍历后面的元素

elem' :: (Eq a) => a -> [a] -> Bool  
elem' y ys = foldl (\acc x -> if x == y then True else acc) False ys

rfold 函数:右折叠

右折叠和左折叠类似,归约函数的参数顺序和左折叠相反

用右折叠实现一个 map 函数

map' :: (a -> b) -> [a] -> [b]  
map' f xs = foldr (\x acc -> f x : acc) [] xs

用左折叠也能实现 map 函数,但是得使用 ++ 连接符,性能较差,因此生成新 List 的时候人们一般都是使用右折叠。

map' f xs = foldl (\acc x -> acc ++ [f x]) [] xs

左折叠和右折叠最大的区别

  • 右折叠可以处理无限长度的数据结构,而左折叠不可以
  • 将无限 List 从中断开执行左折叠是可以的,不过若是向右,就永远到不了头了

foldl1 与 foldr1

foldlfoldr 相似,只是他们假定 List 的首(或尾)元素作为起始值

注意:这里 foldl1foldr1 折叠的 List 中至少要有一个元素,若使用空 List 就会产生一个运行时错误

为了体会 fold 的威力,我们就用它实现几个库函数:

maximum' :: (Ord a) => [a] -> a  
maximum' = 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)

scanl 和 scanr

foldlfoldr 相似,只是它们会记录下累加值的所有状态到一个 List。也有 scanl1scanr1

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]]

优先级提升符号 $

$ 符号相当于为其后的表达式加了一个括号,改变优先级,例如

sum (map sqrt [1..130])
sum $ map sqrt [1..130]

sqrt (3+4+9)
sqrt $ 3+4+9

f (g (z x)) 
f $ g $ z x 

sum (filter (> 10) (map (*2) [2..10])  
sum $ filter (> 10) $ map (*2) [2..10]

此外,用 $ 符号修饰的数据可以调用函数,这个功能在 map 中常用

ghci> map ($ 3) [(4+),(10*),(^2),sqrt]  
[7.0,30.0,9.0,1.7320508075688772]

组合函数

组合函数和数学上的组合函数类似,将几个函数合成一个函数,定义如下

(.) :: (b -> c) -> (a -> b) -> a -> c  
f . g = \x -> f (g x)

有了组合函数,部分使用匿名函数的情况可以简化代码,例如:将列表中的数值全部转换成负数

ghci> map (\x -> negate (abs x)) [5,-3,-6,7,-3,2,-19,24]  
[-5,-3,-6,-7,-3,-2,-19,-24]

改为使用组合函数

ghci> map (negate . abs) [5,-3,-6,7,-3,2,-19,24]  
[-5,-3,-6,-7,-3,-2,-19,-24]

再比如

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

在最后一个参数之前加美元符号,在其他函数之间加点号,这里是另外一个例子;

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]

函数组合可以让柯里化更容易

下面的函数等号两边都有 x ,但是要想柯里化却不是很容易

fn x = ceiling (negate (tan (cos (max 50 x))))

使用函数组合后,就很容易柯里化了

fn = ceiling . negate . tan . cos . max 50

不要炫代码

函数组合、部分函数非常酷,但是有时候会导致代码难以理解,因此尽量写让人容易理解的代码,比如之前“求了小于 10000 的所有奇数的平方的和”的例子

oddSquareSum :: Integer  
oddSquareSum = sum (takeWhile (<10000) (filter odd (map (^2) [1..])))

用函数组合修改为如下代码,一般人不容易理解

oddSquareSum :: Integer  
oddSquareSum = sum . takeWhile (<10000) . filter odd . map (^2) $ [1..]

若是给别人看,我可能就这么写了:

oddSquareSum :: Integer  
oddSquareSum =   
    let oddSquares = filter odd $ map (^2) [1..]  
        belowLimit = takeWhile (<10000) oddSquares  
    in  sum belowLimit

你可能感兴趣的:(06 高阶函数)