文章
Get Programming with Haskell-9
从技术上讲,高阶函数是将另一个函数作为参数的任何函数。
部分内容超过 Notion API 单页读取上限,已尽力加载可访问内容。
# 高阶函数(HIGHER-ORDER FUNCTIONS)
## 1.使用`map`
很难夸大 `map` 函数在函数式编程和 Haskell 中的重要性。`map` 函数接收一个函数和一个列表作为参数,然后**将这个函数应用到列表中的每个元素上**:
```haskell
Prelude> map reverse ["dog", "cat", "moose"]
["god","tac","esoom"]
Prelude> map head ["dog", "cat", "moose"]
"dcm"
Prelude> map (take 4) ["pumkin", "pie", "peanum butter"]
["pumk","pie","pean"]
```
大多数程序员对 `map` 的**第一印象**通常是:它是一个更简洁的 `for` 循环版本。下面这个示例展示了在 JavaScript 中(它同时支持 `map` 和 `for` 循环)使用这两种方法,将限定词 **a** 加到一组动物名称前的两种写法对比:
```javascript
let animals = ["dog", "cat", "moose"]
// 用for循环
for(i = 0; i < animals.length; i++) {
animals[i] = "a " + animals[i]
}
//使用map
let addAnA = function(s) {return "a " + s}
animals = animals.map(addAnA)
```

即使在不像 Haskell 那样严格要求函数式编程的语言中,`map` 依然有不少优势。首先,由于你传入的是一个**具名函数**,你可以**清楚地知道代码在做什么**。在这个简单的例子中可能无关紧要,但 `for` 循环的代码体一旦复杂起来,就不那么容易理解了。如果函数的命名合理,代码的意图就非常清晰。而且,如果你之后想改变 `map` 的行为(比如用一个叫 `addAThe` 的函数代替原来的),你只需要**更换一个参数**就行了。
代码的可读性也得到了提升,因为 `map` 表达的是一种**特定形式的迭代**:你知道它会返回一个新的列表,**长度与原列表完全相同**。这个优势在你刚接触 `map` 或其他列表上的高阶函数时可能还不明显。但随着你逐渐熟悉函数式编程的惯用写法,你会开始\*\*以“如何转换列表”\*\*来思考问题,而不再是以 `for` 循环那种“遍历每个值”的传统思维方式。
## 2.使用map抽象递归
你使用**一等函数**,进而使用**高阶函数**的主要原因,就是为了能够**抽象出编程中的常见模式**。
为了更清楚地说明这一点,我们来看看 `map` 是如何工作的。`map` 表面上看起来像是 `for` 循环的一种简化形式,但实际上,它的内部实现与 `for` 循环完全不同。为了理解 `map` 的工作原理,我们将拿两个可以用 `map` 解决的简单任务,假设**没有 ****`map`****,也没有一等函数**,来手动实现它们。
这两个任务是:
1. 把前面的 JavaScript 示例中的 `addAnA` 行为拿来重现;
2. 实现一个将数字列表中每个元素平方的函数 `squareAll`。
为了更好地理解,你将尝试**复刻 ****`map`**** 的行为**:
```haskell
Prelude> map ("a "++) ["train", "plane", "boat"]
["a train","a plane","a boat"]
Prelude> map (^2) [1, 2, 3]
[1,4,9]
```
我们先从 `addAnA` 开始。你需要问的第一个问题是:“**目标状态是什么?**”因为你是从头到尾遍历列表,所以当你遍历到 `[]`(空列表)时就完成了。接下来的问题是:“**到达终点时你要做什么?**”你是想给列表中的每个元素加上前缀 `a`,但现在已经没有元素了,所以很自然地应该返回一个空列表。另一个暗示你要返回空列表的线索是:你正在**构建一个新列表**。如果你的递归函数最终返回一个列表,它就必须**以空列表结尾**。因此,目标状态(也就是递归终点)的定义很简单:
```haskell
addAnA [] = []
```
唯一的其他情况就是列表是非空的。在这种情况下,你要取出列表的头部 `x`,在它前面加上 `"a "`,然后对剩下的列表递归调用** ****`addAnA`**:
```haskell
addAnA (x:xs) = ("a " ++ x) : addAnA xs
```
这一步是否让你更接近目标?是的,因为你每次都在处理 `xs`,它会一步步变短,最终变成空列表。
`squareAll` 函数遵循了类似的模式。当输入是空列表时,它结束处理。如果输入是非空列表,你就对头部元素进行平方处理,然后递归处理剩余部分:
```haskell
squareAll [] = []
squareAll (x:xs) = x^2 : squareAll xs
```
现在,如果你把这两个函数中各自的拼接操作("a " ++ x)和平方操作(x\^2)抽象成一个通用函数 `f`,你就得到了 `map` 的定义!换句话说,你实现的是:
```haskell
map _ [] = []
map f (x:xs) = f x : map f xs
```
这就是 `map` 的本质——抽象出“对列表每个元素做某件事”这个模式!
如果没有 `map`,你就会一次又一次地**重复编写这种递归函数的模式**。虽然这种“手写递归”并不特别难,但如果频繁地写,**可读性和编程体验都会变差**。如果你觉得递归很难,别担心,好消息是:**任何被反复使用的递归模式,最终都会被抽象成一个高阶函数**。在实际开发中,你**并不需要经常手动写递归函数**。因为那些常见的递归模式,早就已经被封装成各种高阶函数了。所以,当你真正遇到一个需要自己写递归的场景,那通常是一个需要**认真思考的、比较特殊的问题**。