返回文章列表

文章

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) ``` ![](https://prod-files-secure.s3.us-west-2.amazonaws.com/4b1b167f-4b52-4031-8233-f4f2689af574/010604f7-3f47-4a28-bb1e-9879a02fdbe6/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB4667YXC7WYK%2F20261016%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20261016T231627Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECYaCXVzLXdlc3QtMiJIMEYCIQD8dc5FeshqlG%2F3hK8YrdLM4Sjynd54o0nOzfeOsFuv2AIhAPzdfVS88n4EGRCINVv0HWW4nwI%2Bc9FJMIJMiu2VPasSKogECO%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgzjBBxN%2Fm3bBSo7TMkq3AOVPeNjP0%2F898vPH8kFkQ8gOkofbLPXXpGlkjNQUcp3BSpGk7%2F6k%2FXH%2FsETFsEqvvVsqVui9qck6rJJp2T%2BbHTzJ3pw8%2F6JlFFcOU0ePYhbLncTQ25zystxm9nAJTSobsD6b7%2BLjvrIV%2BbHrkFspOtbXJhW%2BnhVBUcRdaAG%2FErWw0RR3N8yWV3B02OYPQlOG%2FzdAO3NW5OfYsTCYZy9RdJKlXiJwqCX9pxIgSSutCRplsKdZAIqY2%2FhHdBxOiYYvUCbuxVHBvYsfnjmueBgDwCDsLA95bTV%2BXCbSwYnE0liywHbRv4mOIPXWGSfc1FB39IhtwCSqjnnMR72HW3aD21okyH8T3uQfT8Kht6Ztb%2BbOdM4yEbO%2Bi73xnw9jVkoSzqWXm0bilylkN608uA%2BbQF39WsjI7DQIpLt%2BqZYja%2FiiO0fWUULfeKizoi9Xf0IV5C7MQ8hafra2RJOWciN2fAkZG0vafODDygFjOorPVcBJp3LVr6txI0UdDue%2BurMHuYppeYpvjz9Wq4zWELgJnW%2FdXhamv9bzAnj7WgSWPuJn%2B%2Fn4o4%2Bep%2BGMcRGlWfWYcHTh0Qyr%2BzJwZIQQim8Boix4Yhkx%2FPKYSyJJ%2B2zZBQ0ZaWFn1vI6WCk%2FwQY7zDdu8rWBjqkAR59J3xGpgNwBkMfySwnuAGhf7lJqKukg65COkdSy0fTVcriftbaXgbQoaqxWcdik3nLJDwuefg9dlQRhi1Rq4cZEXDphbXltgWjES%2B%2F8WKmICNpcf%2BG9o3NwLtusb4G1fMmmk2yc1IGXQVqquVurdJUU%2BhvB4jr7lZ62K%2Bn95L%2FHSHAgEKdwXLAwoqdh0M4vtlTqiwfZs3Uv5d5shyhc1YSDO57&X-Amz-Signature=99d05ee7afbe2bb42f1293bb3e54c6bde18dacf4947bcfd8c832b28894116653&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject#notion_record=block.1d65c571-bb7d-807b-8ce8-cd63798d34c4.4b1b167f-4b52-4031-8233-f4f2689af574) 即使在不像 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`,你就会一次又一次地**重复编写这种递归函数的模式**。虽然这种“手写递归”并不特别难,但如果频繁地写,**可读性和编程体验都会变差**。如果你觉得递归很难,别担心,好消息是:**任何被反复使用的递归模式,最终都会被抽象成一个高阶函数**。在实际开发中,你**并不需要经常手动写递归函数**。因为那些常见的递归模式,早就已经被封装成各种高阶函数了。所以,当你真正遇到一个需要自己写递归的场景,那通常是一个需要**认真思考的、比较特殊的问题**。