Haskell 第一章:公理与函数式思维

公理

公理是搭建数学语言的一套基础工具。公理本身没有对错之分,任何一套自洽的公理体系都可以用于数学证明。对集合论有了解的读者可能知道,ZF 公理体系下无法证明或证否选择公理,这就将数学分为了承认选择公理和不承认选择公理的两个学派。

需要区分公理和定义:公理指出了关于定义的若干无法证明的性质,而在这套定义与公理组成的系统下完成证明可以自由地运用这些公理。

由于 ZFC 公理内容比较复杂,这里不会展开所有内容。在 ZFC 集合论下,皮亚诺公理其实是构造了满足某些性质的集合 \(\mathbb N\) 与函数 \(s:\mathbb N\to\mathbb N\)。函数也是集合:\(s\) 为某个 \(\mathbb N\times \mathbb N\) 的子集,满足对任何 \(x\in \mathbb N\) 都有恰好一个 \(y\) 使得 \((x,y)\in s\),这个 \(y\) 被称为 \(f(x)\)。性质如下:

  • 有一个元素 \(0\in \mathbb N\),使得不存在 \(x\in\mathbb N\) 使得 \(f(x)=0\);
  • 对于任何 \(x\ne y\),\(s(x)\ne s(y)\);
  • 若有 \(S\subseteq \mathbb N\),满足 \(0\in S\),且 \(\forall x\in S,\ s(x)\in S\),则 \(S=\mathbb N\)。

定义 \(0=\varnothing,\ s(x)=x\cup \{x\}\),则皮亚诺公理断言存在 \(\mathbb N\) 满足上述性质,这条断言实际上就是 ZFC 公理中的一条——无穷公理。

这条公理中没有定义加法,只是通过若干性质构造出了一个自然数集,而自然数集上运算的交换律与结合律实际上是定理而非公理。

接下来定义加法:\(+:\mathbb N\times \mathbb N\to\mathbb N\):

  • \(0+x=x\);
  • \(s(x)+y=s(x+y)\)。

接下来证明 \(x+0=x\):\(x=0\) 时 \(x+0=0+0=0=x\);而若 \(x+0=x\),则 \(s(x)+0=s(x+0)=s(x)\)。若有集合 \(S=\{x\mid x\in\mathbb N,\ x+0=x\}\)(根据 ZFC 中的分离公理),则 \(0\in S\),且 \(\forall x\in S,\ s(x)\in S\),则 \(S=\mathbb N\),得证。

这个利用归纳公理的证明方法即数学归纳法。其余加法性质,以及乘法的定义与性质会留作本章的思考题,也会使用数学归纳法作为骨架。

这里有一处逻辑上的漏洞:在证明加法的性质前,首先应说明加法的定义无矛盾且唯一,但这也是数学归纳法的简单应用,可自行完成思考。

函数式思维

在集合论下,所有元素都是集合,在皮亚诺公理的 \(0\) 定义中,\(0\) 既是集合(空集),也是自然数。然而在函数式编程中,将所有元素看作集合会使计算变得无比棘手。可以通过给所有元素赋一个类型来使得计算更加直观。首先自然数 \(\mathbb N\) 为类型。

  • 二元组:若 \(X,Y\) 为类型,则定义 \(X\times Y\) 也是类型,该类型的一个元素由一个 \(x\in X\) 与 \(y\in Y\) 构造得到,记为 \((x,y)\)。那么 \(((1,0),(0,(1,0)))\) 的类型为 \((\mathbb N\times \mathbb N)\times(\mathbb N\times(\mathbb N\times\mathbb N))\),或写作 \(((\mathbb N,\mathbb N),(\mathbb N,(\mathbb N,\mathbb N)))\)。
  • 数列:对于类型 \(X\),由 \(X\) 构成的数列 \(s\) 要么为空,要么为一个元素 \(x\in X\) 再接上一个由 \(X\) 构成的数列,所有 \(X\) 的数列的类型记为 \([X]\)。例如 \([1,2,3]\) 即为 \(1\) 接上(\(2\) 接上(\(3\) 接上空数列)),将一个元素 \(x\) 接上数列 \(s\) 记为 \(x:s\),则 \([1,2,3]=1:(2:(3:[]))\)。
  • 函数:对于类型 \(X,Y\),所有 \(X\to Y\) 的函数的类型记为 \(X\to Y\)。

空序列 \([]\) 的类型可以为任何类型的数列,当然可以将不同类型的空数列看作不同的元素并且在使用时显式地标注类型,但是可以采用一种更巧妙的方法:对任何类型 X,\([]\) 都是 \([X]\) 的元素,则直接定义其类型为 \([a]\),\(a\) 在此处代表一个任意的类型;那么在计算 \(1:[]\) 时,由于 \(1\) 的类型为 \(\mathbb N\),\(x:s\) 要求 \(x\) 的类型为任意类型 \(a\),\(s\) 的类型为 \([a]\),推导得到 \(a=\mathbb N\),则此处 \([]\) 的类型为 \([\mathbb N]\)。同理,如果 \(f((x,s))=x:s\),则函数 \(f\) 的类型就是 \((a,[a])\to [a]\),其中 \(a\) 为任意类型。

考虑一个有两个参数的函数 \(f((x,y))\),其类型为 \((X,Y)\to Z\);可以定义一个新函数 \(g(x)\),其类型为 \(X\to(Y\to Z)\),使得 \(f((x,y))=(g(x))(y)\),将 \(f\) 化为 \(g\) 的过程被称为柯里化。

用符号 \(\lambda\ x \to \cdots\) 表示一个输入 \(x\) 返回箭头右侧表达式的匿名函数。那么可以这样定义柯里化的函数:\(g(x)=\lambda\ y\to f((x,y))\)。

函数之间可以复合。如果有函数 \(f:X\to Y\) 与 \(g:Y\to Z\),可以定义 \(f\circ g=\lambda\ x\to f(g(x))\)。

函数式语言将数学中的函数作为求解信息处理问题问题的基本工具。

函数式思维注重于函数无副作用:副作用即一个函数在多次以相同的参数调用时可能返回不同的结果,无副作用即相同的参数必然产生固定的结果,那么无副作用的函数无法像 c 语言一样修改或读取全局变量。然而 Haskell 中也有输入输出,通过一种巧妙的方式解决输入输出的副作用问题。其具体原理会在后续的 Monad 章节解释,但是其主要思路如下:

考虑将世界抽象为一个变量,其类型为 \(W\)。那么读取一个变量可以看作输入世界,返回读取后的世界与读取的值的二元组。即函数 \(r:W\to (W,\mathbb N)\)。考虑使用 \(r\) 构造一个输入世界,在世界中读取两个数,返回读取后的世界与读取的两个值之和的二元组的函数 \(f:W\to(W,\mathbb N)\)。为了方便,设 \(r(w)=(r_1(w),r_2(w))\)。

\[f(w)=(r_1(r_1(w)),r_2(w)+r_2(r_1(w))) \]

然而函数式程序设计也不是万能的:现代计算机架构本质只能处理有限的信息,最简单的例子是,数学中可以定义实数,而计算机只能保存实数的一个近似。可以定义两个函数相等,但在计算机中判断两个函数是否相等可以规约停机问题。因此函数式程序设计和数学中函数的概念也有区别,对函数的实现也需要结合语言的具体功能完成。

posted @ 2026-11-07 13:26  Yr2X  阅读(18)  评论(0)    收藏  举报