Category Theory and Declarative Programming
Category Theory and Declarative Programming
Category Theory II 1.1: Declarative vs Imperative Approach
第一部分中,讨论了范畴论和编程都是关乎组合的概念。编程过程中,将一个大问题解构成多个子问题,再将这些子问题解构成规模更小的子问题,最终子问题的粒度足够细,从而变成人类能够直接解决的小问题;所有的小问题都解答完毕之后,再将小问题的结果组合起来,一层层向上回溯,最终解决这一原始的、规模较大的问题。一般而言,总是会存在两种编程方式:告诉电脑“做什么/(what to do)”、或者告诉电脑“怎么做/(how to do)”。前者称之为命令式/imperative,后者称为声明式/declarative
考虑一个实例,复合操作本身就可以从声明式和命令式两种途径给出定义。假定函数h
是函数g
及f
的复合,声明式的写法是h = g . f
;而命令式h x = let y = f x in g y
。命令式编程定义了一系列动作,这些动作严格按顺序进行,在函数f
计算完成之前,对g
的调用绝对不可能发生(从代码的概念设计上如此,在某些惰性计算、按需调用的参数传递的语言中,实际执行情况可能存在一些不一致)。
事实上,基于编译器的工作,以上两个版本的代码在实际执行时可能仅有一点差别甚至并无区别。在写出既能解决问题、又有较强维护性和可测试性的代码的过程中,背后的方法论可能完全不一样。
主要问题在于:当我们面对一个难题的时候,是否总是可以在命令式或声明式两种方法中自由选择?若是存在一个声明式风格的解决方案,是否总是可以将之转换成代码实现?这一问题似乎有些偏离主题了,但若能找到答案,很可能会再次革新我们对宇宙万物的理解。
现在通过解释一个物理学概念来演示解决问题的两种方式。物理学中,大部分法则的解释都有两种方式,一种是从局部角度考虑、或者无穷小角度。观测一个规模很小的系统状态,预测在下一个时刻系统状态的变化,通常用微分方程表述,通过对一段时间进行积分或求和等,得出系统状态
这种方式便体现了命令式思想:通过一步步逼近从而到达最终状态,每一步都依赖上一步的结果。事实上,计算机对物理系统的模拟就是将微分方程转换为差分方程,然后进行迭代。宇宙飞船射击类游戏中,飞船的运动便是这样计算出来的。随时间跳动,飞船的坐标都是在进行增量累加,位置增量是由速度和时间得出的,而速度是由初速度、加速度和时间得出,加速度又是由力和质量得出。这一系列方程背后便是牛顿运动定律。对于更复杂的问题,也可以用同样的思想来解答,例如用麦克斯韦方程组来研究电磁场的传播,用晶格QCD(量子色动力学)研究质子内部夸克和胶子的行为等。
数字计算机鼓励了时空离散化与局部化思想的结合。斯蒂芬·沃尔夫拉姆(Stephen Wolfram)曾尝试将整个宇宙的复杂性简化为一个元胞自动机系统,这一行为极致地体现了两者结合。
另一种办法是从全局考虑。观察系统的起始状态和最终状态,然后通过某个函数计算链接这两个状态的最短轨迹。
最简单的例子是费马最小时间原理,它指出光线是沿着传播时间最短的路径传播。一般而言,没有反射和折射发生时,光线从A点到达B点的必定是走的最短路径,即直线路径。光线在透明介质中的传播是比真空中要慢的,若是光线起点位于空气中,止点位于水中,那么尽量让光在空气中跑远一点,在水中跑近一点,这样整体传播时间才更短。所有的经典力学都可以从最小作用原理导出。通过积分动势能之差的拉格朗日函数,可以计算任何轨迹。例如发射一枚炮弹并击中目标,炮弹首先向上飞直到重力势能最大点,然后再转向下飞,势能转化成动能。费马的这些贡献甚至可以用于解释量子机制,这一原理只是关注起始和最终状态,然后计算中间轨迹。
从上看出,在解释物理定律时有两种对偶的办法,可以基于一个局部视角、事件按序发生、事件间进行某些增量的叠加;也可以基于一个全局视角,声明起始状态和结束状态,两状态之间的所有东西就在各自时空位置上安静待着。
全局视角的思考方式也确实可以用在编程上。考虑实现一个光线传播程序,声明光源和观测点,那么就直接可以计算其传播路径。实现过程中并没有显式进行“最短时间”、“最短路径”等相关设置,但是隐含的还是用到了相关物理定理和几何知识。
这两种看待问题的方式最大的不同在于它们对于空间以及更重要的时间的处理方式。局部视角更关注“此时此地”,而全局视角是一个静态视角的长期观测,好像未来是确定的,我们只是在分析某个永恒宇宙的性质。
用函数响应式编程/Functional Reactive Programming来编写用户交互程序时也特别体现全局视角的优越性。常见编程模型是对于所有可能出现的用户输入编写对应的事件处理程序,这些事件处理程序之间可能修改一组共享的可变状态。FRP编程模型外部事件视为一个无限列表,然后在这个列表上应用一系列变换,从概念上讲,所有未来可能出现的动作就在那摆着,等着作为输入进入程序。从程序本身的视角看,这些事件是无序且随机的,对于所有情况,若想得知第N项的状态,那必须先处理完前N-1项。当应用于时序型事件时,我们称之具备因果关系。
范畴论鼓励我们尽可能采用全局视角来看待问题,因此也就是在鼓励声明式编程。首先,范畴论不像是微积分那样有距离、附近、极限、时间等概念,我们所拥有的全部东西就是抽象的对象和对象间的态射。若是能通过一系列步骤从A到达B,那么必定存在从A直接到B的办法。此外,范畴论中的主要工具就是泛构造,泛构造本身也是全局视角看待和解决问题的一个例子。
我们曾用泛构造来定义积,通过声明积的性质即完成。积是一个伴随着两个投影态射的对象,它是个“最好”的对象,因为它能将其他对象及投影进行分解。相对应地,传统的笛卡尔积定义就相当命令式了,需要描述清楚如何从一个集合中选取一个元素、从另一个集合选取一个元素,然后将这两个元素构造成一个序对,这个序对便是积的一个实例。此外,还需要定义解构的方法。
当然,几乎所有的编程语言都直接内置了积、余积、函数类型等概念,而不是通过泛构造来定义(当然也有范畴式编程语言对此进行尝试)。不管是否直接使用,范畴论中的定义都可以证明已有的编程结构是合理的,并且可以产生新的结构。最重要的是,最重要的是,范畴理论提供了一种元语言,用于在声明层次上对计算机程序进行推理。它还鼓励在将问题规范转换为代码之前对其进行推理。