简介:欢迎来到函数式程序设计的世界!
在你以往的编码经验中,你很可能习惯使用指令式程序设计(例如 Python 或 Java),即你需要一步步告诉电脑“该如何”执行任务(例如:“开始一个循环、将数值加到变量中、修改该数值”)。
撰写函数式程序则是一种截然不同的思维模式。你不再是给电脑一份会改变内存数值的“待办事项清单”,而是将程序视为一系列数学上的转换(transformations)。这种方式更简洁、更容易测试,且在处理大数据时非常强大!如果一开始觉得有点“反直觉”也不用担心——一旦你掌握了窍门,就会发现它是多么的优雅。
1. 高阶函数 (Higher-Order Functions, HOFs)
函数式程序设计的秘密武器就是高阶函数。通常我们认为函数的输入是数据(如整数或字符串),但在函数式程序设计中,函数是一等公民(first-class objects),这意味着它们可以像其他任何数值一样被处理。
高阶函数是指具备以下至少其中一项特征的函数:
- 将另一个函数作为参数。
- 将函数作为运算结果返回。
“工具箱”比喻:
想象一个普通函数是一把铁锤。你用它来处理一根钉子(数据)。
而高阶函数就像一只机械手臂。你可以给机械手臂一把铁锤,并指示它去处理房间里的每一根钉子。机械手臂就是那个高阶函数,因为它把“工具”当作输入。
重点总结:高阶函数让我们能够透过将不同的行为“插入”到单一的主要函数中,从而编写出灵活性极高的代码。
2. “三大”高阶函数
AQA 要求你掌握三个专门用于处理列表的高阶函数,分别是 map(映射)、filter(筛选)和 reduce(归纳,有时也称为 fold)。
A. Map (映射)
map 函数会将指定的函数应用于列表中的每一项,并返回一个包含结果的新列表。
示例: 如果你拥有一个数字列表 \( [1, 2, 3] \),并对其执行 map “翻倍”函数,你就会得到 \( [2, 4, 6] \)。
记忆技巧:将 Map 想像成一根魔法棒。你在整排南瓜上挥舞,它们就全都变成了马车。项目的数量不变,但它们的形式改变了。
B. Filter (筛选)
filter 函数会检查列表中的每一项是否符合特定条件(布尔值测试),并返回一个新列表,其中只包含通过测试的项目。
示例: 如果你拥有列表 \( [10, 55, 2, 90] \),并透过 filter 筛选出“大于 50 的数字”,你就会得到 \( [55, 90] \)。
记忆技巧:将 Filter 想像成夜店门口的保安。他们检查“名单”上的每个人,只放行那些符合“条件”(例如:在嘉宾名单上)的人进场。
C. Reduce (或 Fold,归纳)
reduce 函数会将列表“压缩”成一个单一值。它透过重复将组合函数应用于列表项目来达成此目的。
示例: 如果你拥有列表 \( [1, 2, 3, 4] \),并使用“加法”函数执行 reduce,过程会是:\( 1+2=3 \),接着 \( 3+3=6 \),最后 \( 6+4=10 \)。最终结果为 \( 10 \)。
记忆技巧:将 Reduce 想像成一个雪球。当它滚过列表时,它会卷入每一项并将其加入自身,直到最后你得到一个巨大的雪球。
快速回顾:
- Map:项目数量相同,数值改变。
- Filter:项目数量减少,数值不变。
- Reduce:最终结果为单一数值。
3. 函数式语言中的列表处理
在函数式程序设计(如 Haskell)中,列表的结构与你可能习惯的数组不同。一个列表基本上由头部 (Head) 和 尾部 (Tail) 组成。
- Head:列表中的第一个元素。
- Tail:除头部以外,列表中的其余所有内容。
- 空列表:表示为 []。
“火车”比喻:
把列表想像成一列火车。Head 是火车头。Tail 是挂在火车头后面的整列火车厢。如果你把火车头拆下来,剩下的车厢就会变成一列(稍微短一点的)新火车!
常见列表操作:
- Return Head:取得第一个项目(例如:\( [5, 8, 2] \) 的 Head 是 \( 5 \))。
- Return Tail:取得剩余列表(例如:\( [5, 8, 2] \) 的 Tail 是 \( [8, 2] \))。
- Prepend:在前端加入项目(例如:将 \( 10 \) 放到 \( [2, 3] \) 的前端,得到 \( [10, 2, 3] \))。在 Haskell 中通常写作 10:[2, 3]。
- Append:在末端加入项目。
- IsEmpty:检查列表是否为 []。这对于停止递归 (recursion) 至关重要!
你知道吗?函数式程序通常使用递归来代替循环(for/while)。要处理一个列表,函数会处理 Head,然后调用自身来处理 Tail,不断重复直到遇到空列表为止。
4. 重要语言与概念
AQA 提到了一些支持函数式程序设计的语言。你不必精通所有语言,但应该认识它们:
- 纯函数式:Haskell, Lisp, Scheme, Standard ML。
- 多重典范(支持函数式特性):Python, C#, Java 8+, VB.NET。
关键点:不可变性 (Immutability)
在函数式程序设计中,数据是不可变的。这意味着一旦列表被建立,你就不能“修改”它。当你使用 map 时,你并没有改变原始列表,而是建立了一个包含转换后数值的全新列表。这能防止“副作用 (side effects)”,让你的代码更具可预测性!
总结:撰写函数式程序涉及使用高阶函数(map, filter, reduce)来处理不可变列表,并透过将列表拆解为头部与尾部来进行运算。
5. 应避免的常见错误
1. 混淆 Map 与 Filter:学生常在想要移除项目时使用 map。请记住:Map 无法改变列表的长度;只有 Filter 可以做到。
2. 忘记 Tail 也是一个列表:Head 是单一项目(一个整数、一个字符),但 Tail 永远是一个列表,即使列表内只剩下在一个项目!
3. 用“指令式”思维思考:不要试图在函数式程序中使用计数器(例如 \( i = i + 1 \))。请改用“三大”高阶函数或递归。
如果一开始觉得很棘手,别担心!函数式程序设计就像学骑自行车一样——刚开始摇摇晃晃,直到你的大脑突然找到平衡点。练习在纸上写出 map(double, [1, 2, 3]) 的执行过程,这一切很快就会变得自然。