函数式编程范式简介

欢迎来到其中一种最有趣的编程思考方式!到目前为止,你可能已经习惯了程序式编程(procedural programming)(即给电脑一系列逐步执行的指令)或面向对象编程(object-oriented programming)(即创建相互互动的对象)。 在本章中,我们将探讨函数式编程范式(functional programming paradigm)。我们不再逐步告诉电脑“如何”做某事,而是通过使用数学函数,专注于“想要达成什么”。如果起初觉得这有点不同,不用担心——把它想象成学习一种解决难题的新方法吧!

1. 什么是函数式编程?

你过去编写的大部分程序都是指令式(imperative)的。这意味着你会改变程序的“状态”(例如修改变量的值)。 函数式编程则是声明式(declarative)的。在这种风格中,我们将计算过程视为数学函数的求值(evaluation),并避免改变数据。

主要区别:

  • 无副作用(No Side Effects):函数应该只回传一个值。它不应改变自身外部的变量(例如全局变量)。
  • 不变性(Immutability):一旦设定了数值,它就不会改变。与其修改一个列表,不如创建一个带有更改的“新”列表。
  • 无状态(Stateless):程序不像循环计数器那样追踪“当前状态”。
类比:想象一个计算器。当你按下 \( \sin(90) \) 时,它会给你 \( 1 \)。它不会改变计算器的颜色,也不会删除你之前的加总;它只是根据输入给出结果。这就是一个纯函数(pure function)!

快速复习:指令式 = “如何”做。声明式/函数式 = “做什么”。


2. 作为一等公民的函数(Functions as First-Class Objects)

在函数式编程中,函数是一等公民(first-class objects)。这听起来很高级,但其实意思很简单:函数被视为与变量完全一样!

你可以对“一等公民”函数做什么?

  • 将函数指派给变量
  • 将函数作为参数传递给另一个函数。
  • 从另一个函数中回传一个函数作为结果。
类比:将函数想象成一个工具,例如铁锤。你可以把铁锤放进盒子里(变量)、把铁锤交给朋友(作为参数传递),或者让店主递给你一把铁锤(回传值)。

3. 函数应用与组合

为了理解函数如何协同工作,我们使用两个主要概念:应用(Application)组合(Composition)

函数应用(Function Application)

这只是将参数提供给函数的过程。 例子:如果我们有一个函数 \( f(x) = x + 1 \),那么将它应用于数字 \( 5 \),就会得到 \( 6 \)。我们写作 \( f(5) \)。

函数组合(Function Composition)

这是我们结合两个函数以产生新函数的方法。一个函数的输出会成为下一个函数的输入。 如果我们有: \( f(x) = x + 1 \) \( g(x) = x \times 2 \) 组合 \( f(g(x)) \) 意味着我们首先将数字加倍,然后加 1。 如果 \( x = 3 \): 1. \( g(3) = 6 \) 2. \( f(6) = 7 \)

记忆小撇步:将组合想象成接力赛。第一位选手(函数 G)将接力棒(结果)交给第二位选手(函数 F)来完成任务。


4. 定义域、陪域与映射

在函数式范式中,我们使用数学语言来描述数据如何流动。
  • 定义域(Domain):所有可能输入值的集合。
  • 陪域(Codomain):所有可能输出值的集合。
  • 映射(Mapping):将定义域中的输入链接到陪域中输出的规则。
例子:一个将整数(Integer)转换为字符串(String)的函数。 定义域是整数,陪域是字符串。

关键重点:函数 \( f: A \to B \) 意味着函数 \( f \) 将集合 \( A \)(定义域)中的值映射到集合 \( B \)(陪域)。


5. 高阶函数(Higher-Order Functions)

高阶函数是指将另一个函数作为参数、回传一个函数,或两者皆是的函数。这是函数式编程变得非常强大的地方!你需要掌握三个主要的函数:

A. Map(映射)

Map 接收一个函数和一个列表。它将该函数应用于列表中的每一个项目,并回传一个新列表。 例子:对列表 \([1, 2, 3]\) 使用“翻倍”功能,结果为 \([2, 4, 6]\)。

B. Filter(筛选)

Filter 接收一个条件(一个回传 True/False 的函数)和一个列表。它回传一个仅包含符合条件项目的新列表。 例子:对 \([1, 2, 3, 4]\) 使用“是否为偶数?”功能,结果为 \([2, 4]\)。

C. Reduce(或 Fold,归约)

Reduce 接收一个列表,并透过重复应用一个函数,将其“浓缩”为单一值。 例子:对 \([1, 2, 3, 4]\) 使用“相加”功能,结果为 \( 10 \) (\(1+2+3+4\))。

你知道吗?像 Google 这样的大型科技公司使用“MapReduce”来同时处理成千上万台电脑上的海量数据!


6. 列表:头(Head)与尾(Tail)

函数式语言处理列表的方式通常与你习惯的数组不同。它们将列表拆分为两部分:
  • Head(头):列表的第一个元素
  • Tail(尾):一个列表,包含除头部之外的所有其余元素
例子:在列表 \([5, 10, 15, 20]\) 中: Head 是 \( 5 \)。 Tail 是 \([10, 15, 20]\)。 常见错误:学生常以为 Tail 是“最后一个”元素。请记住,Tail 是一个包含所有剩余元素的列表

总结检查清单

你能解释……
  • 指令式声明式编程的区别吗?
  • 函数作为一等公民是什么意思?
  • 组合是如何结合两个函数的?
  • 定义域陪域的定义吗?
  • MapFilterReduce 是如何运作的?
  • 列表的 HeadTail 有什么区别?

如果起初觉得这些很棘手,别担心!函数式编程是一种思维上的转换。继续练习“头/尾”逻辑和高阶函数,很快你就会豁然开朗!