跳转到内容
← 返回核心概念
编程范式计算机科学 · 编程语言16 分钟阅读

函数式编程

Functional Programming

1958 年,约翰·麦卡锡(John McCarthy)在麻省理工学院设计了 Lisp——世界上第一门函数式编程语言。它的灵感不来自硬件或工程实用性,而来自阿隆佐·邱奇(Alonzo Church)在 1930 年代发展的数学系统:Lambda 演算(Lambda Calculus)。 这个血统决定了函数式编程的气质:…

函数式编程不可变性纯函数Lambda演算高阶函数

1958 年,约翰·麦卡锡(John McCarthy)在麻省理工学院设计了 Lisp——世界上第一门函数式编程语言。它的灵感不来自硬件或工程实用性,而来自阿隆佐·邱奇(Alonzo Church)在 1930 年代发展的数学系统:Lambda 演算(Lambda Calculus)

这个血统决定了函数式编程的气质:它首先是数学,其次才是编程。

破除误解:函数式不只是"用函数写代码"

许多人以为"函数式编程"就是多用函数、少用循环。这完全误解了这个范式的本质。

函数式编程的核心思想是:程序是数学函数的组合,而不是机器指令的序列

数学函数有一个关键特性:引用透明性(Referential Transparency)——给定相同的输入,函数始终返回相同的输出,且不产生任何可观察的副作用。这和我们日常说的"函数"不同:

python
# 不是函数式的——依赖外部状态,有副作用
count = 0
def increment():
    global count
    count += 1
    return count

函数式的——纯函数,引用透明 def add(x, y): return x + y ```

纯函数(Pure Function) 是函数式编程的基本单位。它不读写全局变量,不修改传入的参数,不打印到屏幕,不写文件。它只计算并返回一个值。

Lambda 演算:数学基础

邱奇的 Lambda 演算只有三条规则,却是图灵完备的:

  1. 变量$x$
  2. 抽象(函数定义)λx.body\lambda x. \text{body},表示"以 $x$ 为参数的函数"
  3. 应用(函数调用)f  xf \; x,表示"把 $f$ 应用到 $x$"

比如,把两个数相加可以表示为:λx.λy.x+y\lambda x. \lambda y. x + y

邱奇还证明,任何可计算函数都可以用 Lambda 演算表达——这与图灵机等价(邱奇-图灵论题)。因此,函数式语言在理论上与命令式语言有相同的计算能力,但表达方式完全不同。

核心概念

不可变性(Immutability):函数式编程中,数据创建后不再修改。修改数据的唯一方式是创建一个新的数据副本(带有修改内容)。Haskell 的所有变量默认不可变;Clojure 的数据结构默认持久化(persistent)——修改操作返回新结构,共享未修改的部分。

高阶函数(Higher-Order Functions):函数可以接受其他函数作为参数,或返回函数作为结果。最常见的高阶函数: - map:把函数应用到列表每个元素,返回新列表 - filter:用谓词函数筛选列表元素 - fold/reduce:把列表折叠成单一值

haskell
-- Haskell:求列表中所有偶数的平方和
sumSquaresOfEvens xs = foldr (+) 0 (map (^2) (filter even xs))
```

函数组合(Function Composition):把多个函数串联,一个函数的输出成为下一个函数的输入。数学上写作 (fg)(x)=f(g(x))(f \circ g)(x) = f(g(x)),代码中是管道操作符或 compose 函数。这是构建复杂功能的主要方式。

柯里化(Currying):以 Haskell Curry 命名(Curry 本人并未发明,但系统研究了它)。把一个接受多个参数的函数转换为一系列每次接受一个参数的函数:f(x,y)f(x)(y)f(x, y) \rightarrow f(x)(y)。柯里化使部分应用(partial application)成为可能。更准确的出处是:这个思想最早见于 Gottlob Frege(1893),由 Moses Schönfinkel 在 1924 年的论文里系统化,Curry 再加以发展;"currying"这个名字其实是 Christopher Strachey 在 1967 年所造,Curry 晚年得知后曾表示这本应归功于 Schönfinkel。

惰性求值(Lazy Evaluation):直到真正需要值时才计算它。Haskell 默认使用惰性求值,这使得处理无限列表成为可能:

haskell
-- Haskell:定义无限的自然数列表,只取前10个
take 10 [1..]  -- [1,2,3,4,5,6,7,8,9,10]
```

函数式编程的主要语言

语言年份特点
Lisp1958第一门函数式语言,S 表达式语法
ML1973引入类型推断,影响了 Haskell、F#
Haskell1990纯函数式、惰性求值、强类型
Erlang1987面向电信并发,OTP 框架
Clojure2007JVM 上的 Lisp,持久化数据结构
F#2005.NET 生态的函数式语言
Scala2004函数式 + 面向对象的混合
Elm2012前端函数式,无运行时异常

许多主流语言(Python、JavaScript、Java、C++)也在逐步引入函数式特性,如 lambda 表达式、stream API、不可变数据类型。

函数式如何进入主流

很长一段时间里,函数式编程被当作学术界的玩具。真正的转折发生在 2010 年前后:不是哪一门纯函数式语言赢了,而是函数式的思想被主流语言逐个吸收。

Java 8(2014 年 3 月发布) 是标志性事件。它一次性引入了 lambda 表达式、函数式接口(functional interface)和 Stream API,让 list.stream().filter(...).map(...).collect(...) 这样的写法第一次进入企业级 Java。一门十几年里以"啰嗦"著称的语言,开始鼓励用函数组合替代显式循环。

C# 的 LINQ(随 C# 3.0 / .NET 3.5 在 2007 年推出)更早一步,把 WhereSelectAggregate 连同延迟求值(deferred execution)带进了 .NET。这些名字换了壳,本质就是 filter、map、reduce。

JavaScript 与 React 则把函数式带到了最大的开发者群体。React 官方明确要求:组件必须是纯函数——给定相同的 props 和 state,必须返回相同的 UI,渲染期间不得产生副作用,副作用只能放进事件处理器或 Effect。这条纪律不是洁癖,而是 React 并发渲染(concurrent rendering)的前提:因为框架可能多次渲染、甚至中途暂停同一个组件,只有纯函数才不会因此出错。

配套的 Redux 把同样的原则用于状态管理:reducer 必须是纯函数,状态不可变,每次更新返回一个状态对象。这带来两个直接好处——React 能用浅比较(shallow equality)廉价地判断"变没变、要不要重渲染",以及"时间旅行调试"(time-travel debugging)能精确地在历史状态间跳转,因为没有隐藏的副作用。

这才是函数式真正的胜利方式:不是让所有人改用 Haskell,而是让纯函数、不可变、高阶函数成为每个主流语言里的常识。

不可变性与并发:被低估的价值

初学者最大的误解是:"数据不能改,那程序还怎么干活?"在单线程时代,这个质疑听起来有道理。但到了多核与分布式时代,结论反了过来。

并发 bug 的根源几乎总是同一个:可变的共享状态(shared mutable state)。两个线程同时读写同一块数据,就会出现竞态条件(race condition)。命令式编程对付它的办法是加锁(mutex、信号量),而锁本身又带来死锁、性能下降和极难复现的 bug。

不可变性从根上消除了这个问题:如果数据永不改变,多个线程就能同时读它,完全不需要加锁;一个不可变对象可以安全地按引用传给任意线程。麻省理工的并发课程把"实现线程安全"归纳为三条路——封闭(confinement)、不可变(immutability)、加锁(locks)——其中不可变是最省心的一条。

Erlang 把这个思想推到了极致。Joe Armstrong 在爱立信为电信交换机设计的 Erlang,进程之间不共享任何内存,只通过传递不可变消息通信(actor 模型)。没有共享状态,就没有锁,也就没有那一整类并发 bug;配合"任它崩溃"(let it crash)的监督树哲学,造出了能跑多年不停机的系统。今天 WhatsApp 等大规模实时服务的后端正建立在这套模型之上。

MapReduce 是另一个被低估的例子。Google 的 Jeffrey Dean 与 Sanjay Ghemawat 在 2004 年的论文里直言:他们的抽象"受到 Lisp 及许多函数式语言中 map 和 reduce 原语的启发"。正因为 map 阶段的函数是纯的、无副作用,框架才能放心地把同一个计算自动切分到上千台机器上并行执行——这正是后来 Hadoop、Spark 这条大数据技术栈的思想源头。

至于"每次创建新副本会不会太慢"的担心,函数式社区早有答案:持久化数据结构(persistent data structures)通过结构共享(structural sharing),让"修改"只复制变化的那一小部分,其余与旧版本共享,从而把开销降到对数级。Chris Okasaki 1998 年的《Purely Functional Data Structures》是这一领域的奠基之作,Clojure、Scala 的不可变集合都用了同类技术。

单子:函数式处理副作用

纯函数不能有副作用——但真实程序必须读文件、连数据库、打印日志。函数式编程如何处理这个矛盾?

答案是单子(Monad):一种数学结构(来自范畴论),把副作用"包裹"进类型系统,在类型层面区分纯计算和有副作用的计算。Haskell 用 IO a 类型标记所有有副作用的操作,并通过 do 语法糖让代码仍然可读。

单子的解释极难,难到社区里有一句广为流传的调侃:"任何人一旦真正理解了单子,就丧失了把它讲清楚给别人听的能力。" Brent Yorgey 在 2009 年把这种现象命名为"单子教程谬误"(the Monad Tutorial Fallacy):教程作者往往直接抛出抽象,却跳过了自己当初建立直觉时反复挣扎的那些具体例子。本质上,单子是一个容器,定义了如何把一系列操作串联起来。

代价与争议

学习曲线:函数式思维对习惯了命令式的程序员来说需要思维重构。"如何用函数式写一个 for 循环"是初学者的常见困惑(答案是递归或 fold)。

性能开销:不可变数据结构需要在每次"修改"时创建新副本,可能产生大量短命对象,对垃圾回收器造成压力。现代函数式运行时(如 Haskell 的 GHC)有高度优化的内存管理,但与手工优化的命令式代码相比仍有差距。

调试困难:点自由(point-free)风格的代码——即不显式命名参数、只组合函数——可能极难阅读和调试: ``haskell process = filter (> 0) . map (*2) . take 100 `` 这段代码的意图明确,但对不熟悉的人来说像天书。

纯函数式的现实局限:Haskell 社区有一个持续的争论:单子和类型类(type class)系统带来的复杂性,是否值得纯函数式带来的好处?部分人转向了 Rust 这类"借用了函数式思想但不强制纯函数"的语言。

跨域连接

  • 并发与并行:并发缺陷的根源是可变的共享状态。数据永不改变,多个线程就能同时读而无需加锁,一整类竞态随之消失。代价是每次"修改"都产生新对象,靠结构共享只复制变化的那一小部分,把开销压到对数级。推论是:线程安全的三条路——封闭、不可变、加锁——里,只有不可变不需要协调。
  • 范畴论:引用透明真正买到的是等式推理——表达式可以像代数一样被替换成它的值。编译器据此才敢做惰性求值、公共子表达式消除与重排;副作用一旦引入,这条替换规则立刻失效,所有基于它的变换也随之非法。
  • 语义学:组合性原则说,整体的意义由部分的意义及其组合方式决定。纯函数正是让程序满足组合性,而副作用相当于让一个词的意义依赖上下文之外的世界状态——于是局部理解不再可能,必须通读全局。推论是:把副作用集中到少数边界处,其余部分才重新获得可局部推理的性质。
  • 柏拉图主义:把程序看作对与执行无关的数学真理的描述,不只是气质问题。若含义独立于求值顺序,编译器与运行时就有权改变顺序——并行、惰性、失败重算都由此获得授权。这是一条哲学立场直接兑换成的工程自由度。
  • 认知心理学:点自由风格省去参数命名,而命名恰恰是人理解代码的主要抓手。可检验推论:复杂度相同时,取消中间命名会抬高理解成本——可读性之争的根源不在美学偏好,而在工作记忆能同时持有多少个未命名的中间量。同理,函数式真正进入主流靠的不是语言更替,而是纯函数与不可变成为各语言的常识。

参考文献

  • Abelson, H. & Sussman, G. Structure and Interpretation of Computer Programs. MIT Press, 1996. (SICP,最好的函数式入门之一)
  • Wadler, P. Monads for Functional Programming. 收录于 Advanced Functional Programming, Springer, 1995.
  • Church, A. A Formulation of the Simple Theory of Types. Journal of Symbolic Logic 5(2), 1940. (Lambda 演算的基础论文)
  • Dean, J. & Ghemawat, S. MapReduce: Simplified Data Processing on Large Clusters. OSDI, 2004. (明言其抽象受 Lisp 等函数式语言 map/reduce 原语启发)
  • Okasaki, C. Purely Functional Data Structures. Cambridge University Press, 1998. (持久化数据结构与结构共享的奠基之作)
  • React 官方文档. Keeping Components Pure. react.dev. (组件必须是纯函数,并发渲染的前提)

延伸阅读

  • Hutton, G. Programming in Haskell. Cambridge University Press, 2016.
  • Bird, R. Introduction to Functional Programming Using Haskell. Prentice Hall, 1998.