返回文章列表

文章

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=ASIAZI2LB466VNGW7YZZ%2F20260827%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260827T181110Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEHEaCXVzLXdlc3QtMiJIMEYCIQDZJgqQpm8MNY8wgq6f1m0%2BfGEggwdfehM8WCLGIAAVNAIhAPGXQGhdNKUhA8W9%2BzkRqbWZb3z3mbuC0oiEWmpBaNz%2BKv8DCDoQABoMNjM3NDIzMTgzODA1IgwoR6lNCABUfEtpxFMq3AO9YVuVYupVpMxc8Srm%2BCbwkC2jBJ43OG1vIdeA%2B02JIL%2FS3s6ZUBc%2FyiOf4%2Far4OFP8QAtAE07HTj9DA5nXvNmUsepI8MHR2rwZpEWUjbQYBrntnWtbKcuDEpLp3GKkxYmz6diDHnLXCUEH0my7SPotFxTZUMbgUwIIw3wnwuF4EGNbacw7PFkj4Ib83Yj6Y7oryiEaQumq1gsGsdFmlxN5ONOhW6m5r%2Ba0INvJ44QygSyZC%2FD5V97LLkQcUImybfW3y8kCuUlyOyRvgk1RL3Lt07l3itL0OjZ8fCcYtplixJ9nAVENYFMgc%2BbnqdOG5itsNL7wGpoJqx5RJ6sk%2BSHHt9Wozj5udp22Yd66NB716gOxZ86fp6rpdEWxfClZVdgcdtqvdRmItf7QW74FE2FpQR%2Fh%2BaCAz1yC6ozLgbhHV98YOD%2B3tNDY1QEls8nU1%2BPJJpa9xNS3P9cyshAoBsM9tcDd%2Fhr90my%2BoYhfCIW6ti8TMSA8bY4Ff9DOThZ7rrrxzuwJVldswZ%2BkwoT1QB7mDaGo8cOLM%2BsGpnbHmw3%2BUFxBXnRrSmBKI3vYWPgXD93CPmWxutprA4YgFcOxR8KGpZDyWbAc9o6UASx4pQVp9ffQ6GuZqNpackZuzDX0MHUBjqkAVPLFKmPVtnsprMfK8V4AyU51HwKcp68rwY0F3nEE%2FOeIUlCA%2FlE4kLRV4Prnc3B9%2BUO9uJbrib433LGPMScKJY2sm%2BYGrA4mUZ75GOUAWz2RqYl14viH2cm7v5DdAiY5r4mhxfhQpooaiOuo1jM2gKWb7W0n%2BFjIsu6xdyNKOhYva5hlI2zNm16wPIqdGZ5zNr7I%2FRB1KM5GL2rET%2Bv%2FvhgsPh6&X-Amz-Signature=eabb5b16ac51dfb944af57696f196933330970fe997a5937e1859014ef0de408&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) 即使在不像 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`,你就会一次又一次地**重复编写这种递归函数的模式**。虽然这种“手写递归”并不特别难,但如果频繁地写,**可读性和编程体验都会变差**。如果你觉得递归很难,别担心,好消息是:**任何被反复使用的递归模式,最终都会被抽象成一个高阶函数**。在实际开发中,你**并不需要经常手动写递归函数**。因为那些常见的递归模式,早就已经被封装成各种高阶函数了。所以,当你真正遇到一个需要自己写递归的场景,那通常是一个需要**认真思考的、比较特殊的问题**。