<译>声明式编程

原文地址:https://bartoszmilewski.com/2...

这是Bartosz Milewski关于范畴论的博客的第二部分,第一部分已经由 garfileo翻译完成,建议大家先看第一部分。第一部分导言的地址是 写给程序员的范畴论

第二部分的导言

在本书的第一部分我曾说范畴论和编程都与可复合性相关。在编程时,你总会不断地把问题分解到一个你能处理其细节的程度,然后一个个地解决每个子问题,最后把它们自底向上地重新组合起来。大致来说,这有两种实现的方法:告诉计算机要做什么(what to do),或者告诉它如何去做(how to do it)。也就分别是声明式(编程)和命令式(编程)。

你甚至可以从最底层的地方来理解这件事。复合本身可以被声明式地定义,例如hfg的一个复合:

h = g . f

或者命令式地定义,这时,先调用f,保留计算结果,再对该结果调用g

h x = let y = f x
      in g y

一个程序的这种命令式版本通常被描述成按照时间顺序执行的一个指令序列。特别是,对g的调用不能发生在f执行完成之前。至少这是一种概念上的想象————不过,在一个参数传递方式为按需调用的惰性语言中,实际的执行顺序可能完全不同。

实际上,声明式代码和命令式代码的执行过程只会有一点差异,甚至没有,这取决于编译器有多聪明。但这两种方法论毕竟不同,尤其在我们寻找问题的解决方案时和考虑代码的可维护性和可调试性时,它们会完全不同。

这里有一个重大问题:当面对一个具体问题时,我们是否总是可以在声明式和命令式的方法中选择一个?进一步,如果有一个声明式的的解决方案,是否一定可以转化为计算机代码?这个问题的答案远非显然,并且,如果我们能够找到这个问题的答案,我们对宇宙的理解可能就会迎来一场革命。

让我来详细说说。物理中有一个类似的二元性,它要么会是一些潜在的深刻原则的一个重要部分,要么会告诉我们一些有关大脑如何工作的事。理查德·费曼曾经提到,这个对应启发了他在量子电动力学领域的工作。

大部分的物理定律有两种表达形式。一种使用局部(local)或无穷小(infinitesimal)的思维。我们会在一个小邻域内观察系统的状态,并且预测它在下一时刻如何演变。这种想法通常透过一组的微分方程组的形式表达,并且我们会在一个周期时间里对它们做积分或求和。

让我们看看这种形式和命令式思维有多像:我们通过一系列的“小碎步”达到最终解,而每一个碎步取决于前一步的结果。事实上,物理系统的计算机仿真也是按部就班地把微分方程组重写为差分方程组,然后迭代它们。这也是行星游戏中宇宙飞船的运动方式。在每一个时间步长里,飞船的位置通过一个小的增量改变,它就是速度乘以时间间隔。而速度呢,也是如此迭代计算,它的小增量正比于加速度,也就是力除以质量。

牛顿运动定律所对应的微分方程组是有明确写法的:

F = m dv/dt
v = dx/dt

同样的方法可以用来处理更复杂的问题,比如用麦克斯韦方程组来描述电磁场的传播,或者甚至是用格点量子色动力学来描述一个质子中的夸克和胶子的行为。

数字计算机的使用促进了这种与时空离散化相结合的局域思维,史蒂芬·霍夫曼曾试图将整个宇宙的复杂性约减为一个元胞自动机系统,这种思维在这个伟大尝试中体现到了极致。

另一种是全局的方法。我们观察系统的初始状态和终止状态,然后通过最小化某个函数计算出两者之间的轨迹。最简单的例子就是费马的最小时间原理。它声称光线会沿着时间最小的路径传播。特别地,当没有反射物或折射物时,光线会沿着最短的路径传播,也就是一条直线。但是,光在厚实(透明)的材料中会传播的更慢,比如水或玻璃。所以如果你选择的起点在空气中,终点在水里,那么光就会在空气中传播更长一点以获得水中路线的缩减。这个最小时间所对应的路径使得光在空气和水的界面处发生折射,这导出了斯涅尔折射定律:

sin θ_1 / sin θ_2 = v_1 / v_2

其中,v_1是光在空气中的传播速度。v_2是光在水中的传播速度。

和翻译第一部分的 garfileo一样,我也遇到了不能在代码块中写下标的问题,同时思否的LaTeX也不能写行内公式。于是这里采取了和他一样的约定方式: _表示下标, ^表示上标。

所有的经典力学(结论)都能从最小作用量原理推出。该作用量就是拉格朗日量沿路径的积分(对时间),而拉格朗日量则是动能与势能之差(注意是差而不是和,和是总能量)[因此这个所谓的“作用量”是一个关于路径的函数,而路径是一个坐标关于时间的函数,所以作用量是一个泛函,而经典力学下粒子的路径就是这个泛函取最小值的那个路径。想要更具体了解该推导过程,还需要了解变分法的相关内容。译者注]。当你朝一个目标发射一个迫击炮时,炮弹会先上升,这时重力势能变大,从而,对作用量积累负向的贡献。同时炮弹会变慢,在抛物线的顶点达到动能的最小值。然后它会加速以更快地通过接下来的低势能区域。

费曼地最大贡献就是意识到了最小作用量原理可以推广到量子力学。再一次,这个问题被表达成了已知初始状态和终止状态的形式。两个状态间的费曼路径积分被用作计算转移概率。

我们的重点是,在描述物理的定律时有一个奇特的难以解释的二元性。我们既可以用局部的观念,于是万物都按照一个小增量的序列行进;我们也可以用全局的观念,一旦我们声明了初始和终止条件,那么这两者中的过程也就确定了。

全局的手段也可以用在编程中,例如在实现射线跟踪时。我们声明(观察的)眼睛位置和光源位置,需要做的是指出这两者间所有可能的光路。我们不需要精确的最小化每条射线的飞行时间,我们会利用斯涅耳定律和反射定律来达到同样的效果。

局部和全局的手段最大的不同就是在于它们对于时空的处理上,尤其是时间。局部的手段永远对这里和当下感兴趣,而全局的手段会做一个长程的静态的观察,就好像未来早就注定了,我们只不过实在分析某个永恒的宇宙的属性罢了。

没有什么比用函数式响应式编程(the Functional Reactive Programming, FRP)来实现用户交互更能说明问题的了。FRP不是针对所有的可能的用户动作写一个又一个的独立的接口,并让所有人都有那些共享的可变状态的权限,而是把外部事件看作一个无限长的列表,然后对这个列表做一系列的操作。观念上来讲,这个包含所有未来动作的列表就已经在那儿了,就像程序的输入数据一样可以使用。一个数字π的列表和一个伪随机数的列表,又或是一个包含了来自电脑硬件的鼠标位置的列表,在一个程序看来没有任何不同。不论是哪种情况,如果你想要获得(列表的)第n项,那你就必须先获得前n-1项。当这个性质用到瞬时事件里时,我们就称之为因果律

可这和范畴论有什么必然关系呢?我会说明范畴论契合了一种全局的手段并且因此支持了声明式编程。首先,不像微积分,范畴论没有内置的距离或领域的概念,或者时间的概念。我们有的是抽象的对象和他们之间的抽象的连接。如果你能通过一系列的步骤从A得到B,那么你只用一步也可以。此外,范畴论的主要武器是泛构造,而这本身就是全局手段的象征。我们已经在实践中见识过它了,比如,在范畴积的定义中。积定义的方式,就是指明它的性质——一种极其声明式的手段。积是一个具有两个投影的对象,并且是最好的那一个——它优化出了某个性质:能够因式化其他(也满足这种条件的)对象的投影的性质。

注意把它和费马的最小时间原理或最小作用量原理比较。

相反,让我们把它和更加命令式的传统的笛卡儿积定义作比较。你在描述如何生成一个积的元素时,会先从一个集合中挑一个元素,再从另一个集合中挑一个。这是生成一个序对的方式,然后也会有一个拆解序对的方式。

几乎在每一种编程语言中,包括函数式语言,例如Haskell,积类型、余积类型和函数类型都是内置的,而不是通过泛构造定义的;即使已经存在一些创造范畴式的编程语言的努力(例如,参见参考文献中Tatsuya Hagino的论文)

不论是否被直接使用,范畴化的定义支撑了已有的编程结构,并且会引导出一些新的结构。更重要的是,范畴论在声明式的层面上提供了一种可以生成计算机程序的元语言。这也鼓励我们在实现代码之前就能够导出问题的范式。

致谢

感谢Gershom Bazerman检查我的数学和逻辑,和André van Meulebrouck在编辑上的帮助。

参考文献

  1. The Catsters, Products and Coproducts video.

下一篇:极限与余极限

你可能感兴趣的:(范畴论,haskell)