最近在 Hacker News 上看到一个很有意思的问题:社会是不是因为“逼着”数学家重新发明了自己的领域,反而很走运?乍看这是一个偏哲学、偏历史的话题,但如果你从计算机、软件工程、算法设计这些角度切入,会发现这个问题其实非常“技术”。数学家因为计算机的出现,被迫把原来建立在无限、连续、纸笔推导之上的思维,改造成有限、离散、可计算、可验证的一套新体系。这个“被迫重塑”的过程,恰好催生了现代计算机科学里最核心的工具:从离散数学到算法复杂度,从数理逻辑到形式化验证。
本文不打算做哲学辩论,而是把这个话题拆成一条可学习、可实践的技术路线。我会先解释数学家为什么要“重塑”自己的领域,再梳理数学在计算机里的几个关键分支,然后用 Python 和 Lean 4 分别演示“用代码验证数学性质”和“用证明器验证数学定理”,最后给出常见的坑、工程最佳实践以及后续学习方向。无论你是刚接触编程的初学者,还是已经在后端、算法、安全领域工作的开发者,这篇文章都能帮助你找到“数学”与“代码”之间真正实用、可落地的连接点。
1. 背景:数学家如何被“逼”着重塑领域
1.1 问题由来与“重塑”的含义
标题里的“reinvent their field”听起来很重,但放在数学史上并不夸张。19 世纪和 20 世纪上半叶的数学,很大程度上围绕“连续性”“无限”“存在性”展开。很多证明只要说明某个对象“存在”就算完成,比如“存在一个实数满足某某性质”,至于这个实数能不能被构造出来、需要多少步骤、在计算机上能不能执行,并不是当时的核心问题。
让数学家改变思路的核心推动力来自计算机。20 世纪中叶,图灵、丘奇、哥德尔等人把“计算”本身变成数学研究对象。这时候人们突然意识到:一张纸上成立的“存在性证明”,在真实计算机上并不一定能落地。香农的信息论、冯·诺依曼的计算机架构、高德纳的算法分析,都在不断提醒数学家——你们需要一种更“工程化”的数学,而不是只追求优雅和抽象。
这里说的“重塑”至少包含三层变化:
- 从“是否存在”走向“如何构造”:计算机要求算法不仅要证明某个结果正确,还要给出一步一步执行的过程。
- 从“无限连续”走向“有限离散”:计算机内存和算力有限,连续数学必须离散化,微积分变成了差分方程,实数变成了浮点数。
- 从“纸笔推导”走向“机器可验证”:数学证明本身也可以变成一段程序,由机器检查每一步推导是否合法。这就是后来发展出的形式化验证。
从历史结果看,这次被迫重塑不仅没有毁掉数学,反而让数学进入了一个更广阔的应用时代。社会确实因为这种“被迫”大大受益。
1.2 数学与计算的关系转变
数学与计算的关系,可以粗略分成三个阶段。早期,数学计算是手工的,比如牛顿用手算微积分,欧拉用算盘和纸笔推进数值分析。接着,计算机出现后,数学开始为计算服务,算法设计和数值分析成为独立学科。到最近十几年,计算反过来为数学服务,机器开始帮数学家证明定理、搜索反例、验证复杂推导。
这三个阶段并不是后者替代前者,而是同时并存。今天的算法工程师,既要懂微积分、线性代数等连续数学,也要懂图论、组合数学、数理逻辑等离散数学。而形式化验证工程师,则要同时理解编程语言、类型论和证明论。
如果你是一名开发者,可能已经感受到这种“重塑”的痕迹。比如你写一个for循环,本质上是在构造一个数学归纳过程;你写一个递归函数,需要考虑终止性;你分析一个算法的时间复杂度,是在做一种渐近分析。这些都不是语文题,而是数学题。因此,理解数学家被迫重塑的这个过程,能让你更清楚地知道:代码世界里那些看似琐碎的规则,背后其实站着一整套被“重新发明”的数学体系。
2. 数学在计算机科学中的关键分支
2.1 离散数学与算法思维
计算机世界里几乎没有真正的“连续”,即便处理音频、图像,最终也是采样和离散化。所以,计算机科学最底层的数学分支不再是微积分,而是离散数学。离散数学包含集合论、数理逻辑、图论、组合数学、代数结构等内容。
举个例子,判断两个字符串是否互为变形词,最直接的方法是排序后比较。这个思路背后是“排列”和“排序稳定性”的概念。再看路由算法,Dijkstra 最短路径依赖图论里的“松弛定理”。你在数据库里做 join,也和集合运算、关系代数有关系。一个没有离散数学基础的程序员,通常只能“调用现成函数”,遇到性能瓶颈或边界条件时很难给出系统性的解法。
算法思维的核心是:把问题抽象成数学结构,再选择合适的数据结构和复杂度可接受的算法。这个过程不是背 API,而是做“数学建模”。很多从传统数学转过来的开发者会得心应手,正是因为他们习惯了抽象与证明;但传统数学里“反例”“边界条件”的重视程度明显比工程世界低,因此也需要主动补齐。
2.2 数理逻辑与可计算性
数理逻辑可能是最被低估的一个数学分支,但它恰恰是计算机科学的理论基石。图灵机停机问题、哥德尔不完备定理、布尔代数、谓词逻辑、类型论,这些内容看起来非常抽象,却直接决定了“什么能被计算”“什么能被证明”“程序如何被编译”。
现代编程语言越来越依赖类型系统,而类型系统的理论根基就是类型论——一种数理逻辑的现代形态。你写一个接口,相当于定义一个逻辑命题;你实现这个接口,相当于给出这个命题的构造性证明。这种“命题即类型、证明即程序”的看法,在函数式编程社区里尤其流行。比如 Haskell 的很多设计、Rust 的所有权系统,都能从类型论中找到思想来源。
从工程角度看,数理逻辑还能帮你排查复杂 bug。当你遇到“状态爆炸”或“并发死锁”问题时,本质上是在处理有限状态机中的可达性问题。这些问题都可以用逻辑公式表达,再用模型检测工具自动求解。
2.3 数值计算与误差控制
并非所有数学都适合离散化。工程领域里大量问题仍来自连续数学,比如物理模拟、信号处理、机器学习训练。这些场景需要用数值计算方法逼近真实解,于是“误差分析”成了关键能力。
一个简单的浮点数例子:在 Python 中计算0.1 + 0.2,结果不是0.3,而是0.30000000000000004。这不是程序 bug,而是二进制浮点数无法精确表示十进制小数的必然结果。如果开发者不了解浮点数误差,就可能写出if a + b == 0.3这样永远不成立的逻辑。同理,机器学习中的梯度下降、数值积分、求解微分方程,全部要面对舍入误差、截断误差和稳定性问题。
这部分实际上也是数学家被“逼”出来的分支之一。经典微积分可以假设“无穷小量”存在,但数值分析必须关心“误差是否可控”。作为工程师,掌握误差控制不是让你去手推每个公式,而是要培养一种警惕性:代码中的数字未必是数学中的实数。
3. 核心工具拆解:从数学命题到代码
3.1 用 Python 验证数学性质
当我们想验证某个数学猜想在小范围内是否成立时,代码是最好的工具。Python 以简洁、生态丰富著称,特别适合做原型验证。
下面我们以哥德巴赫猜想为例,写一个验证小程序。哥德巴赫猜想的内容是:任何一个大于 2 的偶数,都可以表示成两个素数之和。这个猜想尚未被完全证明,但我们可以写代码对小范围数据进行验证。
# 文件路径:goldbach.py def sieve(n: int) -> list[bool]: """埃拉托斯特尼筛法,返回 [0, n] 内每个数是否为素数。""" is_prime = [True] * (n + 1) is_prime[0] = is_prime[1] = False for i in range(2, int(n ** 0.5) + 1): if is_prime[i]: for j in range(i * i, n + 1, i): is_prime[j] = False return is_prime def check_goldbach(limit: int) -> bool: """ 验证从 4 到 limit 的偶数是否都能分解成两个素数之和。 若所有偶数都满足,返回 True,否则打印反例并返回 False。 """ is_prime = sieve(limit) primes = [i for i, v in enumerate(is_prime) if v] prime_set = set(primes) for even in range(4, limit + 1, 2): found = False for p in primes: if p > even: break if (even - p) in prime_set: found = True break if not found: print(f"反例: {even}") return False return True if __name__ == "__main__": result = check_goldbach(1000) print(f"哥德巴赫猜想在 [4, 1000] 范围内验证结果: {result}")运行这个脚本,输出应为:
哥德巴赫猜想在 [4, 1000] 范围内验证结果: True这段代码体现了几个重要的数学与编程交叉点:
- 筛法利用合数的性质,把素数判断从“逐个试除”优化为“批量标记”,这是算法思维对数学枚举的加速。
- 在循环中提前
break,利用了“一个偶数若能分解为两个素数,必然存在一个不超过它的一半的素数因子”这一性质,避免无效遍历。 - 用集合
prime_set做 O(1) 查重,这一技巧来自数据结构设计。
这里要注意,代码验证并不等于数学证明。即使limit设成 1 亿,得到True,也不能说明哥德巴赫猜想成立。它只能作为辅助工具,帮助我们发现反例或支持进一步研究。这也是“构造性数学”与“存在性数学”之间一个很微妙的差异。
3.2 用 Lean 4 做形式化证明
如果说 Python 验证是一种“试验”,那么形式化证明就是让机器严格检查每一步推导。Lean 4 是近年来社区活跃度很高的一个证明助手,它基于依赖类型论,支持数学定理的机器验证。
先看一个最简单的自然数加法定理。在 Lean 4 中,我们可以这样写:
-- 文件路径:Basic.lean theorem zero_add (n : Nat) : 0 + n = n := by rfl theorem add_comm_example (a b : Nat) : a + b = b + a := by exact Nat.add_comm a b- 第一行定义了一个定理,名字叫
zero_add。它表达的内容是“对于任意自然数 n,0 + n 等于 n”。 by rfl表示“根据定义左右两边完全相同”,因此证明直接完成。- 第二行定义了一个例子,内容是加法交换律
a + b = b + a。 Nat.add_comm a b是 Lean 标准库中已经证明好的自然数加法交换律。exact表示“直接用这个定理作为当前目标的证明”。
如果你在 VS Code 中打开这个文件,将鼠标光标放到by后面,Lean 会实时显示证明状态。如果证明正确,界面不会报错;如果证明缺失或错误,会产生一条红色错误信息。
Lean 4 的学习曲线比 Python 陡峭得多,但它的价值在于:机器不会忽略边界条件,不会跳步,也不会被“直观显然”蒙混过关。这就要求用户具备很强的形式化思维,也正是数学家被迫重塑的方向之一:把“显然”变成“可检查”。
3.3 数学建模与算法翻译
从数学到代码,不只是写一个函数,还需要“建模”。以“求最大公约数”为例,数学定义是:两个整数 a 和 b 的最大公约数是能同时整除 a 和 b 的最大正整数。用代码实现时,我们既可以用穷举法,也可以用欧几里得算法。后者基于一个数学定理:gcd(a, b) = gcd(b, a mod b)。这就涉及从数学定义到递推算法的翻译。
# 文件路径:gcd.py def gcd(a: int, b: int) -> int: """欧几里得算法计算最大公约数。""" while b != 0: a, b = b, a % b return a print(gcd(48, 36)) # 输出 12 print(gcd(17, 5)) # 输出 1这个例子虽然简单,但它展示了“数学定理如何变成可终止的算法”。循环每执行一次,b都会变成a % b,它一定小于原来的b,所以算法必然终止。这种“终止性”证明,在形式化验证和程序语义分析中非常重要。
数学建模能力是连接数学与编程的桥梁。一个合格的后端工程师,在接到“求两个大数的最大公约数”这类需求时,不会直接写:
def gcd_slow(a: int, b: int) -> int: ans = 1 for i in range(1, min(a, b) + 1): if a % i == 0 and b % i == 0: ans = i return ans因为一旦数字过大,这个函数会非常慢。而欧几里得算法的时间复杂度大约是 O(log min(a, b))。这就是从数学定义到工程实现之间必须经历的优化过程。
4. 环境准备与示例项目
4.1 环境安装
本文包含两个主要代码示例:Python 和 Lean 4。Python 的安装相对简单,建议使用 Python 3.10 或更高版本。如果你只在本地运行goldbach.py,不需要额外安装第三方库。
Lean 4 的环境搭建则稍微复杂一些。推荐使用elan工具链管理器安装 Lean 4。安装完成后,在 VS Code 中扩展市场搜索“Lean 4”,安装官方扩展。使用时只需要创建一个后缀为.lean的文件,Lean 扩展会自动加载环境并检查证明。
如果你不想立刻安装 Lean 4,也可以先阅读示例代码,理解“证明即代码”的思路,等需要深入学习时再搭建环境。
4.2 创建示例项目结构
我们用一个极简的项目结构来组织示例文件:
math-reinvent-demo/ ├── goldbach.py ├── gcd.py └── Basic.leangoldbach.py:哥德巴赫猜想小范围验证。gcd.py:欧几里得算法演示。Basic.lean:Lean 4 形式化证明示例。
这种结构简单但清晰,可以放在任意目录下运行。
4.3 运行与验证
先运行 Python 脚本:
python goldbach.py python gcd.py预期输出:
哥德巴赫猜想在 [4, 1000] 范围内验证结果: True 12 1然后打开 VS Code,编辑Basic.lean。Lean 扩展会在你输入定理时实时给出反馈。如果文件末尾出现一个绿色的小圆点,表示所有证明均通过;如果是红色波浪线,说明某个证明有问题,需要修改。
一个常见的小技巧:如果你不确定某个策略能不能用,可以在 Lean 4 源码里用#check查看某个定理的签名,例如:
#check Nat.add_commLean 会返回该定理的类型,让你确认它是否满足当前目标。
5. 常见问题与排查思路
5.1 浮点数比较失败
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
0.1 + 0.2 == 0.3返回 False | 二进制浮点数无法精确表示所有十进制小数 | 使用abs(a + b - 0.3) < 1e-9或Decimal |
这个问题在数值计算、测试断言、单元测试中非常常见。如果你是写金融类代码,建议直接使用Decimal类型;如果是科学计算,应设置合理的容差范围。
5.2 形式化证明写不出来
Lean 4 的初学者经常遇到“目标看起来显然,但我不知道用什么策略”的问题。通常的排查顺序是:
- 先
#check相关定理,看标准库里是否已有结论。 - 使用
simp尝试自动化简。 - 使用
rw重写目标。 - 如果目标是一个等式,并且两边定义相同,用
rfl。 - 实在写不出时,用
by omega尝试证明自然数线性算术公式,但这需要导入 Mathlib,并不是所有基础环境都默认包含。
不要试图一上来就证明复杂定理。从0 + n = n开始,慢慢熟悉 Lean 的证明状态窗口。
5.3 验证程序出现“反例”
如果你运行哥德巴赫验证脚本并得到“反例”,首先检查你的筛法实现是否正确。常见的 bug 包括:循环边界多算或少算,初始值设置错误,素数集合漏掉2。其次,确认你验证的范围是否从4开始,因为2和3不属于“大于 2 的偶数”。这类问题通常不是数学猜想有误,而是程序逻辑与数学定义不一致。
更广义的教训是:用代码做数学验证时,必须把代码与数学定义逐条对应,任何一处偏差都可能产生假“反例”。
6. 最佳实践与工程建议
6.1 用数学思维设计算法
在项目开发中,数学思维不是“写几个公式”,而是抽象、边界分析和复杂度衡量。
- 设计接口时先定义清楚输入域和输出域,就像数学中先定义函数定义域和值域。
- 每写一个循环,都问自己:这个循环会终止吗?最坏情况执行多少次?
- 遇到复杂业务状态机,先用有限状态机、集合关系等离散结构描述,再写代码。
- 对性能敏感路径,一定要分析算法复杂度,而不是靠“感觉”优化。
很多线上事故,比如死循环、内存溢出、数据不一致,背后都能归结为数学建模阶段出了问题。把数学当成“白板上的设计工具”,而不是考场上的应试内容,你会更容易写出稳健的代码。
6.2 形式化验证的适用边界
形式化验证越来越受重视,尤其是安全关键系统:区块链共识协议、智能合约、航天软件、编译器、操作系统内核等。这些场景一旦出错,经济损失或安全风险极高,因此值得投入更高的验证成本。
但形式化验证并不是银弹。它的开发成本很高,对团队成员数学和编程能力要求苛刻。对于普通业务系统,用单元测试、属性测试、代码评审通常已经是足够好的质量保障。最佳实践是:
- 高风险模块优先使用形式化验证。
- 普通模块使用传统的测试和静态分析。
- 在引入形式化验证之前,先建立完善的测试基线,避免“为了证明而证明”。
6.3 工程中如何沉淀数学资产
很多团队在做算法开发时,只把代码留在仓库里,数学推导、边界假设、复杂度分析全都放在某个人脑子里。一旦这个人离职,后续维护会非常困难。建议在代码仓库中增加一个docs/math-notes.md文件,记录以下内容:
- 算法对应的数学原理和参考来源。
- 输入数据的约束条件,比如整数范围、精度要求、是否允许负数。
- 浮点数误差控制策略。
- 复杂度分析和最坏情况场景。
- 验证方法:单元测试、随机测试、属性测试或形式化证明。
这是一项“低技术含量但高工程价值”的工作。信息密度高,能极大降低维护成本,也是从“会写代码”到“会设计系统”的重要分水岭。
7. 总结与学习路线
回到最初的问题:社会是不是因为迫使数学家重新发明自己的领域而走运?从工程视角看,答案是肯定的。数学家被迫从“存在性证明”转向“构造性证明”,从“连续抽象”转向“离散计算”,从“纸笔推导”转向“机器验证”,这一系列变化直接推动了计算机科学、软件工程和人工智能的发展。作为开发者,我们不需要重新经历数学史,但确实有必要理解这些关键转折背后的思想。
如果你想继续深入学习,建议按这个顺序走一遍:
- 掌握离散数学的核心模块:集合、关系、图论、组合数学、数论基础。
- 学习算法设计与复杂度分析,尝试用 Python 实现经典算法并验证数学性质。
- 了解逻辑学基础:命题逻辑、谓词逻辑、归纳法。
- 接触一个现代证明助手,比如 Lean 4 或 Coq,从最简单的定理开始。
- 在真实项目中实践:把数值计算误差控制、边界分析和复杂度论证养成习惯。
数学不是代码的敌人,而是代码最可靠的设计说明书。动手把文章中的代码跑一遍,比收藏一百个教程都有用。如果你在这个过程中遇到“证明卡住”“浮点数不对”之类的问题,欢迎沿着文中的排查思路继续折腾,很多时候,跨过那个坎的收获比代码本身更有价值。