数学上的闭包概念及与编程的关系

简要介绍了数学上闭包的概念及其性质在编程领域的应用.

目录

首先, 需要强调一点, 这里谈论的 闭包(closure) 概念是指数学上的, 不是我们编程界一般谈论的那个闭包.

在编程实践中, 闭包另有定义, 是一种为表示带有自由变量的过程而用的实现技术.

但另一方面, 这个数学上的闭包概念在编程实践中依然是有体现, 虽然不同于编程界一般谈论的那个闭包, 后面会举一些例子加以说明.

闭包到底是什么?

闭包在数学上是一个比较抽象的概念, 来自于抽象代数, 因此这里不打算直接给出它的定义, 否则大家看了估计还是一头雾水, 为便于理解, 还是先从具体的例子出发, 最后才给出它的定义.

以加法在自然数集合中为例

我们先考察一个很简单的例子, 就是加法在自然数集合中的操作及其结果.

首先, 自然数集这个很好理解, 就是0, 1, 2, 3..., 这些整数的集合, 当然需要注意的一点是它是一个无穷的集合.

然后是加法这个操作, 我们也很熟悉, 它需要两个操作数, 从刚才的自然数集合中任意取出两个数, 然后执行加法操作:

比如, 1 + 2, 3 + 5, 6 + 4 等等

然后这些加法操作会有一个结果, 比如 1 + 2 = 3, 3 + 5 = 8, 6 + 4 = 10 等等, 当我们观察这些结果, 比如 3, 8, 10 等等时, 不难发现它们也还是属于自然数集合.

说到这里, 你可能会想, 这不是显而易见的嘛, 感觉像是说了一堆废话! 先别急着下定论.

至此, 我们就可以初步给出闭包的定义, 首先有一个集合(自然数集合), 然后有一个操作(加法操作), 这个操作需要集合中的两个元素, 最后操作的结果仍然属于这个集合.

因为结果仍然属于这个集合, 我们就说, 自然数集合对于加法操作来说是封闭的(closed).

这个是针对结果来说的, 也就是无论你怎么操作来操作去, 结果都还在集合内, 有点孙悟空怎么翻筋斗也逃不出如来佛手掌心那种感觉.

自然数集合在加法这样一种操作下是封闭的, 我们就说它满足(satisfy)一种闭包性质(clousre property).

更一般化的说, 一个集合被认为是满足一种闭包性质, 如果它在一个(或一系列)操作下是封闭的.

A set that is closed under an operation or collection of operations is said to satisfy a closure property.

另一个操作: 乘法

上面的定义涉及到一个或一系列操作, 下面就说说另一个操作, 比如乘法, 同样还是自然数集合, 随便取两个数, 然后乘起来:

比如, 2 × 3, 3 × 7, 5 × 4 等等.

然后也会有一个结果, 比如 2 × 3 = 6, 3 × 7 = 21, 5 × 4 = 20 等等, 当我们观察这些结果, 比如 6, 21, 20 等等时, 不难发现它们也还是属于自然数集合.

因此, 根据前面的定义, 我们可以说, 自然数集合对于乘法操作来说也是封闭的(closed).

毕竟来说, 乘法在某种意识上讲也是一种加法, 因此自然数集合对其封闭也就不难理解了.

自然数集合对于加法操作来说是封闭的(closed), 同时对于乘法操作来说也是封闭的(closed), 因此, 可以说它在一系列操作(加法及乘法)下都是封闭的.

也即是一个集合可以不仅对一个操作是封闭的, 它还可能对很多操作都是封闭的.

当减法引入时

说完了加法和乘法, 估计有些同学已经不耐烦了, 说, 这有啥稀奇的呢? 难道有什么操作是不封闭的吗? 下面就来说说再另一个操作, 减法, 然后我们会发现, 事情会有一些变化.

如果草率地去看, 还是针对自然数集合来说, 随便取两数, 然后执行减法操作:

比如, 3 - 2, 7 - 4, 5 - 1 等等;

然后也会有一个结果, 比如 3 - 2 = 1, 7 - 4 = 3, 5 - 1 = 4 等等, 当我们观察这些结果, 比如 1, 3, 4 等等时, 不难发现它们也还是属于自然数集合.

从以上来看, 我们似乎也可以说, 自然数集合对于减法操作来说同样也是封闭的(closed).

但只要我们多考察一些情形, 比如 2 - 4, 3 - 6 等等, 就会发现, 不对劲了, 2 - 4 = -2, 3 - 6 = -3, 像 -2, -3 这些结果它们并不属于自然数集合!

在古代, 人们的认知能力还很弱时, 面对像 2 - 4 这么骚气的操作时, 干脆就像鸵鸟把头埋进沙子里一样, 对其视而不见了, 人们认为这样的操作是没有意义的!

于是, 自然数集合对于减法操作来说并不是封闭的. 但如果我们扩大自然数集合, 变成整数集合, 它包含了负整数集合, 0 和 正整数集合.

这时候, 对于一个整数集合而言, 则无论你怎么去取数进行减法操作, 结果总是还在这个整数集合中.

又一次的, 孙悟空怎么翻筋斗也逃不出如来佛手掌心, 这是个更大的手掌心!

于是, 虽然自然数集合在减法的操作下不是封闭的, 但一旦扩大到整数集合, 我们又可以说, 整数集合对于减法操作是封闭的.

另一种情形则是, 对一个有限的集合而言, 比如只有一个元素 0 的集合, 表示为 { 0 }, 我们也照样可以说, 它对于加法, 乘法和减法都是封闭的, 因为 0 + 0 = 0, 0 × 0 = 0, 0 - 0 = 0, 结果无论怎么搞都还是 0, 因此也是封闭的. 这个例子中没有扩大集合的范围, 甚至是相反, 缩小了集合的范围, 也能得出封闭的性质, 当然这就属于比较特殊的情况了.

闭包的另一种定义

前面提到的一个闭包的定义是指集合满足的一种闭包性质, 对于闭包还有另外一种定义.

当一个集合 S 在某些操作下不是封闭的,

比如自然数集合对于减法操作不是封闭的.

我们可以找到一个包含 S 的最小集合使得操作是封闭的,

比如, 可以找到 整数集合 这个包含 自然数集合 的最小集合, 它对于减法操作是封闭的.

那么, 这个最小的集合就叫做 S 集合(针对那些操作而言)的闭包(closure).

整数集合就是自然数集合(针对减法操作而言)的闭包.

具体的英文定义如下:

When a set S is not closed under some operations, one can usually find the smallest set containing S that is closed. This smallest closed set is called the closure of S (with respect to these operations)

这个定义跟闭包性质的定义有差异, 但两者还是有紧密关系的, 只不过是说法的侧重点不同, 但都是围绕着操作结果的封闭性去说的.

当操作继续增加

整数集合对于加法, 乘法和减法都是封闭的, 但如果继续引入其它操作, 比如除法, 那么新的问题又会来了, 虽然像 6 ÷ 2, 10 ÷ 5 等整除的情形, 结果还在集合中, 但更多的诸如 2 ÷ 3, 5 ÷ 8 结果就不在集合里面了.

此时如果想让除法操作封闭, 还得继续扩展集合到比如有理数集合.

此外还得施加一个限制, 除数还不能为 0.

而对于有理数集合, 当继续扩大操作时, 比如引入了开方运算, 很多结果又不是封闭的了, 这时得扩大到无理数集合; 而即便是到了无理数集合, 对于负数的开平方依然是束手无策的, 这时就需要进一步的扩大到复数集合了...

显然的, 当引入越来越多的操作, 想继续的保持封闭性就很困难了.

在编程中的应用

至此, 我们已经说完了数学上的闭包这一概念, 自然也只是蜻蜓点水地说一下, 让大家有个概念而已, 更深的咱们也不懂, 也不打算去涉及, 毕竟太艰深了.

通过以上的介绍, 相信大家对数学上的闭包这一概念多少也有了一些了解, 但很多人可能会问, 去知道这些干啥呢? 对我们编程似乎也没啥用. 而前面一开始也说了, 这里谈的闭包还不是编程界通常谈论的那个闭包, 那这里谈论的这个数学上的闭包对于我们的编程语言到底有什么影响呢?

怎么说好呢, 要说没有影响恐怕是不可能的, 但如果说有多少大的影响恐怕很多人又不认同了, 问题就在于这些影响往往是非常基础性的, 以至于我们不觉得这算什么影响.

举个非常简单的例子, 我们在编程中很可能随手就写出了这样的表达式:

2 + 4 + 5

而从不去问到底为什么, 甚至说这为什么是可能的, 我们觉得这些似乎是天经地义, 不值一提的.

但从闭包的角度去看, 2 + 4 + 5 之所以成为可能, 是因为 2 + 4 的结果仍然属于集合之内, 才因此可以继续参与下一次的操作.

当我们例举另外的例子, 比如另一个表达式:

(3 > 2) + 1

情况就有点微妙了. 对于有些编程语言而言, 这样的操作仍然是可行的, 但其实是因为包含了一种隐式的转换; 而对于另外的一些编程语言来说, 这可能就是语法错误了.

从闭包的角度去看, 3 > 2 这个操作的结果并不属于数的集合, 而是一个只有两个有限元素的 boolean 集合: {true, false}.

因为这个操作的结果并不是一个数, 所以后续的操作其实就没有意义了.

比如 true + 1, 或者是 false + 1 都是没有意义的了.

有些编程语言之所以能够继续操作下去, 是因为内部做了隐式转换, 比如把 true 转为 1, false 变为 0, 所以继续执行的其实是 1 + 1 或者 0 + 1.

有些语言限制会比较严格, 比如 java, 你写这样的语句是要报错的:

if (age = 60) {
    // 可以退休了
}

这里的一个陷阱就是, 单个的等号是一个赋值的操作, 而不是比较, 比较是用两个等号 == 这样:

if (age == 60) {
    // 可以退休了
}

这样写的 java 程序才能编译通过, 但有的语言两种写法都能编译, 但第一种情况实际会变成:

if (60) {
    // 可以退休了
}

但 if 其实要求的是一个 boolean 集合的操作数, 而此刻会执行一次转换, 一般而言, 0 会转成 false, 其它则是 true, 所以 60 就转换为 true, 最后语句实质变成了:

if (true) {
    // 可以退休了
}

这样一来, 整个判断就完全多余了, 结果总是为真, 里面的语句始终都能执行, 在很多情况下, 这就是一个程序的 bug 了.

可见, 不严格的遵循闭包性质可能会给我们带来麻烦; 另一方面, 设计良好的闭包性质有可能带给我们便利.

比如说, 有的语言允许数组的元素还可以是数组, 这就给很多的操作带来了很多方便, 这其实是闭包性质的一个体现.

而有的语言则不允许这样, 数组的元素只能是其它元素, 但不能是数组, 这就削弱了语言的表达能力, 进而在编程中给我们带来不便.

又比如, 对于很多语言来说, 对象的属性可以继续是对象, 又或者说, map 的元素还可以是另一个 map, 这样一来, 我们就可以嵌套地表达很多复杂的数据结构, 而对它们的操作又可以比较简化, 因为操作的结果还在集合内, 就可以反复运用同一种操作去操作它.

举个例子来说, 一颗树的节点可以是一个叶子节点, 还还可以是一棵子树, 对于文件夹及里面的文件及(或)文件夹而言就是这么一种情形, 因此可以用同一个操作递归地遍历所有的节点.

而上述这些, 往深了说, 其实都是都是闭包性质的体现, 因为这些性质的存在, 才使得这些成为可能.

对于我们的编程实践来说, 某种组合数据对象的操作满足闭包性质, 那就是说,通过它组合起数据对象得到的结果本身还可以通过同样的操作再进行组合.

闭包性质是任何一种组合功能的威力的关键要素, 因为它使我们能够建立起层次性的结构, 这种结构由一些部分构成, 而其中的各个部分又是由它们的部分构成, 并且可以如此继续下去.

你也许觉得这些是天经地义, 理所当然的, 原因也许在于它过于本质, 过于基础了, 以致于你都没有觉察到它, 但它还是在那里发挥着它的作用.

我觉得深入的理解这些概念是有助于我们成为一个更好的程序员的, 这也是这篇文章介绍这一概念的原因, 另外的一个原因则是, 大量的介绍闭包概念的文章讲的都是编程上的闭包, 而不是数学上的闭包.

当然这点对于我们程序员而言也是无可厚非的, 毕竟我们更关注编程相关的, 只是我觉得, 既然这个概念有多重的解析, 我们多了解一些也不无坏处.

当然由于我也不是什么深入研究数学的, 这也仅是一篇介绍性的文章, 免不了挂一漏万, 如果你有什么意见或建议, 欢迎留言, 关于数学上的闭包概念及其在编程中的影响就介绍到此.