函数式编程的核心在于表达式
熟悉函数式编程将一切视为表达式的处理方式。
函数式编程的核心在于将表达式求值为值。对于初学者来说,这一概念可能一开始难以理解。通过与命令式编程进行对比,可以更好地掌握它。
在图灵奖演讲中,《编程能否摆脱冯·诺伊曼风格?一种函数式风格及其程序代数》,约翰·巴克斯(John Backus)指出,命令式编程语言(如 C 和 Java)将语言元素分为两个世界:表达式和语句。表达式是那些能够求值为值的构造。在 C 或 Java 中,我们可以定义算术表达式如 1 + 2、布尔表达式如 true || false,以及字符串表达式如 "Hello"。它们都会求值为一个值。
另一方面,语句是执行某些操作的命令,例如变量赋值 s = s + 1、if 语句用于分支,以及 for/while 循环用于重复执行语句。语句的特点是具有副作用。
函数式编程语言没有语句,没有变量赋值,没有 if 语句,也没有 for/while 循环。因此,代替变量赋值,我们通过函数参数和返回值来传递值。条件判断如 if 被当作表达式而非语句。为了实现重复计算,函数式编程依赖递归函数,而不是循环。在函数式范式中,一切都是表达式。
下图展示了命令式编程与函数式编程在世界观上的差异。
难怪函数式编程对初学者而言会显得奇怪——它要求我们彻底摒弃语句的概念。
由于在函数式范式中一切都是表达式,学习一门函数式编程语言的核心在于掌握如何构建表达式,并将它们组合成更复杂的表达式。我们将讨论 OCaml 中的算术表达式、布尔表达式和字符串表达式。接着,我们会探讨 if 表达式,这是函数式编程对命令式语言中 if 语句的替代方案。
算术表达式
OCaml 提供了整数的内置字面量,如 1 和 2。这些字面量通常被称为原始表达式,因为它们代表最简单的表达式形式。
OCaml 提供了内置运算符:+、-、*、/ 和 mod,用于加法、减法、乘法、除法和取余运算。我们可以用这些运算符构建复合表达式。
11 + 2
其中,1 和 2 称为操作数。+ 运算符称为二元运算符,因为它接受两个操作数。
复合表达式本身也可以作为操作数,从而构建任意复杂的算术表达式。例如:
1(1 + 2) * (3 - 4 * 5)
OCaml 还为浮点数提供了专用运算符。要对浮点数进行加、减、乘、除运算,我们使用 +., -., *., 和 /.。
13.14 *. 2.0 *. 2.0
注意:在 OCaml 中,我们可以写
2.而不是2.0。这意味着上面的表达式可以改写为:
13.14 *. 2. *. 2.
布尔表达式
OCaml 还提供了比较运算符,如 =、>、>=、< 和 <=,用于比较两个表达式。例如:
11 = 2 2 3(1 + 2) * (3 - 4 * 5) > 6 + 7
比较运算符生成布尔表达式,其结果为 true 或 false。我们可以使用逻辑运算符 not(否定)、&&(与)和 ||(或)将布尔表达式组合起来。例如:
1true && (1 > 2) 2 3((1 + 2) * (3 - 4 * 5) = -51) || (1 > 2) 4 5not (true && (1 > 2))
当然,布尔表达式的嵌套深度没有限制。此外,请注意虽然我们见过的大多数运算符都是二元的,但 not 是一元运算符的一个例子,因为它只接受一个操作数。
字符串表达式
和大多数编程语言一样,OCaml 中的字符串字面量(如 "Hello")用引号括起来。我们可以使用 ^ 运算符来连接两个字符串。
1"Hello " ^ "OCaml"
if 表达式
任何强大编程语言的关键特性之一,是能够测试条件并根据结果选择不同的计算路径。命令式编程语言使用 if 语句来表达“如果某条件为真,则执行此操作,否则执行另一操作”。下面的 Java 代码展示了 if 语句的典型用法,用于计算两个数 a 和 b 的最大值:
1if (a > b) { 2 max = a; 3} else { 4 max = b; 5}
如你所见,max 变量的值会根据条件更新。
但在函数式范式中,if 是一种表达式形式:if e1 then e2 else e3。正确的理解方式是:如果 e1 求值为 true,则整个表达式的结果是 e2 的值;否则,结果是 e3 的值。例如:
1if 1 > 2 then 1 else 2
在 OCaml 的 if 表达式中,else 分支是必需的,因为整个 if 表达式被视为一个表达式,意味着无论条件结果如何,它都必须求值为一个值。
由于条件部分以及 then 和 else 分支都可以包含表达式,if 表达式可以被深度嵌套,包含任意复杂的表达式,甚至包括其他 if 表达式。例如:
1if 1 = 2 then if (1 + 2) * (3 - 4 * 5) = -51 then 100 / 6 else 5 - 1 else 42
从 if 语句到 if 表达式的思维转变,是理解函数式编程世界观的第一步——一切皆为表达式。
请使用下方的代码沙箱尝试 OCaml 表达式。注意最后一行 let _ = ... 会将表达式的结果打印到控制台。根据你输入的表达式类型,你可能需要相应调整。
1(** TODO: 将 [expr] 的定义替换为你想尝试的内容 *) 2let expr = 3 if 1 = 2 then if (1 + 2) * (3 - 4 * 5) = -51 then 100 / 6 else 5 - 1 4 else 42 5 6(** 打印 'expr' 的值 *) 7(** 如果 'expr' 是浮点型,可使用 OCaml 内置的 'print_float';如果是字符串,使用 'print_string'。 8 要打印布尔值,使用 'print_string (string_of_bool expr)' *) 9let _ = print_int expr
组合的力量
你玩过乐高吗?许多孩子和成年人喜欢花几个小时用乐高积木搭建房屋、机器人和人形模型。
乐高之所以如此吸引人,是因为我们可以用同一组积木以无穷无尽的方式组合,创造出几乎任何东西,唯一的限制只是我们的想象力。乐高惊人灵活性的秘密其实很简单:从一组有限的简单积木出发,我们可以将它们组装成更复杂的积木,再进一步组合成更复杂的结构,如此不断延伸。
这与函数式编程的艺术有何关联?因为同样的魔法也适用于表达式!事实上,像 OCaml 这样的函数式编程语言提供了一组有限的基本构造,用于构建原始表达式。就像乐高积木一样,原始表达式可以无限组合,形成更复杂的表达式。
函数式编程范式之所以能充分发挥组合的力量,正是因为它完全依赖表达式作为基本构建单元。然而,在命令式编程范式中,组合能力受到严重限制,原因正如本课开头所述:表达式和语句被划分为两个独立的世界。诸如变量赋值、if 语句或循环等语句与表达式之间的组合性很差。
让我们看一个具体的例子。正如我们在前一节看到的,函数式编程语言将条件表达式如 if 1 > 2 then 0 else 42 视为表达式。正因为如此,我们可以轻松地将其与其他表达式组合。例如,if 1 > 2 then 0 else 42 可以作为 + 运算符的操作数。
但在命令式编程语言中,由于 if 是语句而非表达式,我们无法这样做。
这个例子虽然很小,但它清楚地表明:函数式编程通过将一切视为表达式,在组合性方面具有显著优势。