欢迎来到函数式程序设计的世界!

在你至今接触过的大多数程序设计中,你可能都在使用命令式范式 (Imperative paradigm)——即一步步告诉电脑“如何”去做某事(就像食谱一样)。在本章中,我们将探索函数式程序设计范式 (Functional programming paradigm)。与其编写一连串的指令,我们将程序视为一系列数学转换的过程。

如果起初觉得这听起来有点“数学味”,别担心!我们将透过你已经熟悉的简单概念,例如自动贩卖机和乐高积木,来深入浅出地讲解这些概念。

4.12.1.1 函数类型 (Function Type)

在函数式程序设计中,函数 (Function) 是一条将一组输入映射至一组输出的规则。我们使用以下符号来表示:
\( f: A \rightarrow B \)

这个符号是什么意思?

  • \( f \):这只是函数的名称。
  • \( A \):这是定义域 (Domain)。它是所有可能“输入”值的集合。
  • \( B \):这是对应域 (Co-domain)。它是从中选择“输出”值的集合。

规则:对于定义域中的每一个输入,函数都会在对应域中指派唯一一个输出。但请注意,对应域中的值并不一定全部都要被用到!

例子:想象一个告知你字母在字母表中位置的函数。
定义域:{a, b, c... z}
对应域:{0, 1, 2... 25}
如果你输入 'a',函数会将其映射到 0。如果你输入 'b',则映射到 1。

快速回顾:定义域 vs. 对应域

试想一台自动贩卖机
定义域是你按下的按钮(A1, B2 等)。
对应域是机器里的所有零食。即使机器刚好卖完了薯片,但“薯片”仍然属于对应域,因为它们“可能”作为一种输出。

重点总结:定义域是你的数据来源;对应域则是可能的结果“池”。

4.12.1.2 一等对象 (First-class Objects)

在函数式程序设计中,函数是一等对象 (First-class objects)。这是一个比较正式的说法,意思是函数被视为与其他数据类型(如整数或字符串)完全相同。

如果某物是一等对象,它可以:

  1. 出现在表达式 (Expressions) 中。
  2. 指派给变量 (Assigned to a variable)
  3. 作为参数 (Argument) 传递给另一个函数。
  4. 作为函数调用的结果被返回 (Returned)

你知道吗?在 Python 或 Haskell 等语言中,你可以直接将变量设为一个函数,例如:my_function = print。现在,my_function("Hello") 的运作方式就和 print 指令完全一样!

重点总结:一等对象是程序语言中的“VIP”公民——它可以出现在任何地方,并像普通数据一样被使用。

4.12.1.3 函数应用 (Function Application)

函数应用就是将输入(参数)提供给函数的过程。
例子:如果你有一个名为 add 的函数,那么 add(3, 4) 就是该函数对参数 3 和 4 的应用

“单一参数”的秘密

尽管我们可能会说像 add(x, y) 这样的函数接受两个参数,但在函数式程序设计中,技术上将其视为仅接受一个参数:一对数字。
在数学上,我们这样写: \( f: \text{integer} \times \text{integer} \rightarrow \text{integer} \)
符号 \( \times \) 代表笛卡儿积 (Cartesian product),这只是一种称呼“一对整数”的华丽说法。

重点总结:应用函数意味着用真实数据来“触发”它。

4.12.1.4 偏函数应用 (Partial Function Application)

这部分非常酷!由于函数是一等对象,我们可以只给函数“部分”参数。这会返回一个新的、特化的函数,它在等待剩余的参数。

类比:想象一台咖啡机。
1. 你放入“咖啡胶囊”作为参数。
2. 机器还不会给你饮料;它现在变成了一台特化机器,正在等待“水”这个参数。
3. 一旦你加入水,流程就完成了。

标记法

让我们看看 add 函数:
通常情况: add(x, y)
在偏函数应用中: add: integer \( \rightarrow \) (integer \( \rightarrow \) integer)

如果我们调用 add 4,它会返回一个新的函数,该函数会将你给它的任何数字加上 4。如果你随后给这个新函数一个 6,最终结果就是 10。

避免常见错误:

不要将偏函数应用与普通的函数调用搞混。在普通调用中,你想要一个最终答案(如 10)。在偏函数应用中,你是刻意建立一个新的、更简单的函数以供稍后使用。

重点总结:偏函数应用将一个需要多个参数的函数,转化为一系列每次仅接受一个参数的函数。

4.12.1.5 函数复合 (Composition of Functions)

函数复合 (Functional composition) 是将两个函数结合起来建立一个全新函数的过程。你将第一个函数的输出作为第二个函数的输入。

如果我们有两个函数:
\( f: A \rightarrow B \)
\( g: B \rightarrow C \)
我们可以将它们组合成一个新函数:\( g \circ f \)

重要:在符号 \( g \circ f \) 中,右边的函数(\( f \))先执行!其结果随后会传递给左边的函数(\( g \))。

现实生活中的例子:

想象两个函数:
1. f(x):将数字加 2。
2. g(x):将数字取立方 (\( x^3 \))。

复合函数 \( g \circ f \) 就是: \( (x + 2)^3 \)。
如果你输入 1:先加 2(结果为 3),然后取立方(最终结果为 27)。

记忆法: “Fog”法则

当你看到 \( g \circ f \) 时,可以把它倒着读,或者将其想成 "g of f"。这就像一条生产线——数据从右边的函数流入左边的函数。

重点总结:复合就像将乐高积木拼在一起。一个“积木”的输出必须完美契合下一个积木的输入,才能构建出更大的东西。

本章总结 - 快速回顾

  • 函数类型:由其定义域(输入)和对应域(可能的输出)定义。
  • 一等对象:函数可以像任何其他变量一样被处理(传递、返回等)。
  • 函数应用:使用带有特定输入的函数。
  • 偏函数应用:仅提供部分参数以建立一个新的、特化的函数。
  • 复合:将两个函数(\( f \) 和 \( g \))结合,使前者的输出成为后者的输入(\( g \circ f \))。