可逆计算理论中的可逆到底指的是什么?

可逆计算理论是我受到物理学和数学的启发,在2007年左右提出的新一代的软件构造理论。可逆一词与物理学中熵的概念息息相关,熵增的方向确定了物理世界中时间箭头的演化方向,可逆计算理论所研究的是面向演化的粗粒度软件结构的构造规律,所以可逆正是这个理论的点睛之笔。一些没有学过热力学和统计物理的同学,对于熵的概念一无所知,看到可逆这个说法难免会感到一头雾水。可逆重要吗?软件要怎么可逆?逆向执行吗?这有什么意义?在本文中,我简单解释一下可逆计算理论中可逆到底指什么。

一. 可逆计算的核心公式

可逆计算提出了软件构造的一个核心公式

ini 复制代码
   App = Delta x-extends Generator<DSL>

需要明确指出的是,可逆计算理论是一个具有严格定义的科学理论,它并不是一个仁者见仁、智者见智的方法论,这里的公式也不是一种示意性的类比,它实际上包含如下精确定义的内容:

  1. Delta、Generator和DSL都具有Tree结构,而且我们会明确定义一个坐标系统,用于在Tree结构中进行精确定位和依赖追踪
  2. x-extends是Tree结构上的通用的差量合并算子,它具有精确的数学定义,并且可以在数学上证明它满足结合律
  3. Generator是作用在DSL结构空间上的函子(Functor)映射,它负责将一类结构映射为另一类结构(注意,不是专用于某一个对象,而是可以应用到领域内的一大类结构上)

Nop平台是可逆计算理论的一个参考实现,它就是将数学上的抽象符号和定义翻译为具体的实现代码而已。

传统的软件工程理论中也有增量开发的概念,但是并没有一种系统化、精确化的技术形式去表达这个所谓的增量,特别是没有建立具有明确定义的逆运算和逆元的概念,在一般人的概念中增量就是增加的意思,这与可逆计算理论中的差量概念有着本质性区别。

与很多人所想象的不同,可逆计算理论中的可逆指的并不是逆向执行的意思。实际上,因为一般情况下我们总是在编译期应用可逆计算,此时根本没有到运行期,也就不存在什么逆向执行的问题。

因为可逆计算的核心公式具有明确的数学定义,所以所谓的可逆性也完全体现在这个公式所蕴含的数学操作中。

二. 逆元:通过增加实现减少

可逆计算中的可逆第一个明显的体现是包含逆元的基本结构单元: Delta 。传统上软件构造的基本单元是没有逆元概念的,我们所有能够操作的事物都是的元素,而逆元是累加上去之后会使得原有的东西消失的元素。

可逆计算中的Delta是一个复杂的差量化结构,如果分解到最细粒度,它相当于是各种正的原子元素和负的原子元素的混合体。

在我们的物理世界中,一切物质都可以分解为少数的一些基本粒子,而所有的粒子都有与它自身相配对的反粒子。粒子和反粒子可以从虚空中创生出来,也可以相遇湮灭化为那一道光。

在考虑存在单位元的情况下,任意元素总可以被看作是作用在单位元之上的某个差量, <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 + A = A 0+A=A </math>0+A=A。所以说,全量是差量的一个特例,我们可以在差量概念的基础上重新认识和构建所有软件结构。

在数学上,元素和作用在元素之上的运算是作为一个整体被定义的,也就是说Delta和x-extends算子必须作为一个整体被考虑,只有在明确x-extends作用含义的情况下我们才能清楚的解释Delta中的逆元到底意味着什么。

三. 减法:方程的逆向求解

当我们从操作的角度去理解逆元的时候,就可以很自然的从逆元概念得到减法。

css 复制代码
  A = B + (-C) = B - C

如果引入了减法的概念,我们就可以通过移项实现方程的逆向求解

css 复制代码
 A = B + C ==>  B = A - C

在Nop平台中的实际实现为

ini 复制代码
App = Delta x-extends Base ==>  Delta = App x-diff Base

DeltaMerger.javaDeltaDiffer.java分别实现了正向的x-extends和逆向的x-diff运算。也就是说,在Nop平台中通过Delta合并算法合并得到整体之后,我们还可以通过diff算法反向计算重新拆分出其中的Delta成分,这种能力使得我们可以像解方程一样实现软件的差量化构造。

具体的一个实际应用场景是,既支持自动化模型生成,又支持可视化设计的前端页面设计器。在Nop平台中,前端页面是根据数据模型自动推定生成的,在自动生成之后我们可以通过可视化设计器对生成的结果进行微调,然后再保存回页面DSL的时候,我们应用diff算法计算得到可视化修改的差量部分。也就是说,如果可视化设计器只是进行了局部调整,则实际保存到DSL文件中的也是少量的差量信息,而不是整个完整页面的信息。通过这种方法,我们可以实现自动化模型生成和可视化设计之间的协同作用。

ini 复制代码
PageInEditor = DeltaPage x-extends Generator<ViewModel>
DeltaPage = PageInEditor x-diff Generator<ViewModel>

在可视化编辑器中进行编辑的总是全量页面 PageInEditor,但是保存到文件系统中的DSL是如下结构,它只突出显示手工修改的局部差量。

yaml 复制代码
x:gen-extends: |
  <web:GenPage view="NopAuthUser.view.xml" page="main" xpl:lib="/nop/web/xlib/web.xlib" />

title: 修改后的标题

四. 可逆变换

目前主流的软件构造理论都是基于组合、组装的观点,而少有系统化的基于变换的观点来考察软件系统的构造问题。但是正如我在知乎的文章解耦的方法远不止依赖注入中所指出的,为了实现解耦,最本质、最强大的手段应该是表象变换,接口只是表象变换的最简单的一种形式而已

回想一下数学物理中最重要的Fourier变换理论,多个不同频率的信号叠加在一起,在时域上看来它们是完全混杂在一起的,在每一个时间点上实际上都存在着多个信号的影响。但是通过Fourier变换,我们在频域上可以得到完全分离的多个信号!

在可逆计算理论中明确包含 Generator<DSL>这样一个对应于产生式编程的计算单元。DSL是对业务领域信息的一种高效表达,而Generator相当于是根据DSL表达的信息应用一个形式变换,得到这一信息的其他表现形式(在此过程中有可能会引入新的信息或者对已有的信息进行剪裁)。这里的一个特殊情况,就是Generator存在逆变换的情况。

有趣的是,虽然可逆变换是一般性的形式变换的一种特殊情况,在实际的应用中,它却可以系统化的解决大量实际问题。

甚至,在理论层面上说,我们可以在可逆变换的框架下解决所有计算问题,毕竟量子计算就是一种可逆变换。

假设变换G存在逆变换F,则我们可以实现A和B之间的双向转换

css 复制代码
B = G(A), A = F(B)

低代码平台中的可视化设计问题可以在可逆变换的框架下自动被解决

scss 复制代码
可视化表象 = Designer(文本表象),   文本表象 = Serializer(可视化表象)

可视化设计器读取文本表象,产生可视化表象,从而支持用户通过可视化手段编辑模型,然后用户保存时,再将可视化设计器中的模型信息序列化成文本,导出到文件中就成为DSL模型文件。

可逆性可以复合:如果结构的每个子部分都支持可逆变换,则我们可以自动推导得到一个应用于结构整体的可逆变换。例如,如果我们对每个字段级别的结构定义了可视化表象和对应的文本表象,那么我们就可以自动推导得到整个DSL文本和可视化设计表单之间的双向转换。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ( a a − 1 ) ∗ ( b b − 1 ) = ( a b b − 1 a − 1 ) \left(\begin{array}{c} a\\ a^{-1} \end{array} \right)*\left( \begin{array}{c} b\\ b^{-1} \end{array} \right) =\left( \begin{array}{c} ab\\ b^{-1}a^{-1} \end{array} \right) \\ </math>(aa−1)∗(bb−1)=(abb−1a−1)

我们可以根据a的逆和b的逆,自动推导得到ab的逆为 <math xmlns="http://www.w3.org/1998/Math/MathML"> b − 1 a − 1 b^{-1}a^{-1} </math>b−1a−1。

需要注意的是,在实际应用中,我们很多情况下遇到的只是相似变换,并不是完全一致的等价变换。比如说,在DSL的可视化编辑中,为了展示美观,可视化表象中可能会包含一些专用于可视化表象的布局信息,这些信息对于DSL文本表象而言是完全多余的内容。在这种情况下,为了保证我们在DSL文本编辑和可视化编辑之间自由切换,我们可以应用可逆计算理论的差量化思想,为文本表象补充一些扩展的Delta结构,专用于存储这些额外的信息,将约等于的相似关系转化为严格的相等关系。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> B ≈ G ( A ) , A ≈ F ( B ) ⟹ B + d B = G ( A + d A ) , A + d A = F ( B + d B ) B \approx G (A) , A \approx F(B) \Longrightarrow B + dB = G(A + dA), A + dA = F(B + dB) </math>B≈G(A),A≈F(B)⟹B+dB=G(A+dA),A+dA=F(B+dB)

在Nop平台中,充分贯彻了差量化设计的思想,在所有的结构中都内置了扩展属性机制,也就是说,永远采用 (data, ext_data)这种配对设计。在技术形式上,ext_data经常作为元数据来存储。比如在消息对象设计中体现为 data + headers,而在Java类设计中,体现为 field + annotations,在DSL模型设计中,体现为 props + ext_props

另外需要强调的是,数学上的可逆变换远比一般人想象中的函数可逆映射要复杂、强大得多。在Nop平台中的可逆变换大部分相当于是范畴论中的函子变换(Functor)。也就是说,它不是作用于单一的某个对象上,而是定义在领域(所谓的范畴)中的每个对象上。举例来说,Nop平台提供了DSL对象和Excel之间的可逆转换:无需编程,可以解析Excel文件得到DSL对象,同时也可以将DSL对象导出为Excel文件。

ini 复制代码
DslModel = Importor<Excel>, Excel = ReportExporter<DslModel>

传统的编程实践中,我们总是针对某一个具体的Excel文件编写解析代码,然后解析得到某一个特殊的数据对象,反向也是同样的情况,我们针对某个具体的业务对象编程,实现一个Excel导出函数。如果我们现在针对某个需求编写了这里的双向转换函数,后来需求变了,或者我们开发一个新的产品,那么我们只能是参考以前的导入导出代码,但是没法直接复用它们,必须针对新的对象结构重新编写。

但是,在Nop平台中导入模型和报表模型构成一对非常通用的可逆变换,它可以自动实现将任意的数据对象 导出为Excel文件,同时也可以解析任意的一个Excel(只要满足相当宽泛的Tree结构条件即可)得到对应的数据对象。用范畴论的术语来描述,相当于Nop平台是建立了 DSL范畴和Excel范畴之间的一对伴随函子(Adjoint Functor)。这种函子化的可逆变换如果进一步进行复合,可以形成一种前所未有的强大技术手段。

在实际的业务应用中,如果我们总是追求最小化信息表达,强调业务开发的框架中立性,那么很自然的就会导向可逆形式变换。具体分析参见我的文章业务开发自由之路:如何打破框架束缚,实现真正的框架中立性

五. 沿时间线逆行

传统软件工程中的构造手段都是沿着时间线正向进行的。比如说,在概念层面上,我们需要先开发基类A,然后再开发派生类B,基类A总是先于派生类B存在。当我们进行扩展时,我们一般是增加不同的派生类来引入新的信息结构。少数情况下,我们会修改基类A,然后就需要重新编译,相当于重新构造所有的派生类。如果放到时间线上去考量,我们会看到信息是逐层堆叠,层层累进的,旧的信息在下方,而新的信息在上方。如果我们要把新的信息注入到系统的根基中,那么就必须要修改源码,相当于是把结构打碎然后重新构建

可逆计算的构造公式作为一个纯数学的形式表达,它的各个部分并没有明确的时间依赖假定。

css 复制代码
App[t] = Delta[t-1] x-extends Generator[t]<DSL[t]>

t时刻需要构建的系统,可以由t-1时刻确定的Delta,作用到t时刻才确定的Generator[t]<DSL[t]>结构上,合成为一个整体构成。因为t时刻(比如部署时刻)我们已经知道所有业务相关信息,可以避免猜测用户应用场景中的大量可能情况,从而只需要描述当前需要用到的信息,继而我们可以用一个根据t时刻信息定制的Generator来动态展开这些信息。

落实到具体的代码实现中,Nop平台提供了一种非常独特的Delta定制能力。它可以在完全不修改现有源码的情况下,对系统中所有用到的DSL结构进行差量化调整。类比Java中的实现我们可以更清楚的看到Delta定制的独特性。假设我们现在已经构建了一个系统 A extends B extends C,把它们打成了jar包。现在在完全不修改这个jar包的情况下,Java中是没有任何手段修改这个已有的结构的,我们只能是在外部补充新的信息

java 复制代码
E extends A extends B extends C

但是如果利用Delta定制机制,我们可以通过补充一个delta描述,任意替换已有的代码结构。例如将替换B为E+F,得到 A extends E extends F extends C。在数学层面上相当于是

css 复制代码
X = A + B + C
Y = X + (-B + E + F) = A + E + F +C

Delta定制的能力体现在可逆计算公式的如下推演中

ini 复制代码
App = B + G<S>
App + dApp = B + G<S> + dApp = B + dB + (G + dG)<S + dS>

在可逆计算理论的构造公式中,扰动差量可以产生于任何部分,然后这些差量可以跨越当前已有的结构,统一汇总为一个外部修正dApp。这种数学上的构造要求扰动信息可以在已有结构之间自由流动。它的一个特殊应用就是将t时刻才知晓的信息,以差量化的形式直接注入到已有的、t-1时刻就固化的程序结构中,此时无需修改已有的源代码。类似于我们永远可以有一枚后悔药,让我们穿越到t-1时刻,对程序结构进行任意的适配调整,最终解决我们在t时刻遇到的问题

需要注意的是,虽然Docker技术也采用了App = Delta x-extends Generator<DSL>这样一种计算模式,是可逆计算理论的一个典型应用实例,但是它并不支持Nop平台中的这种动态Delta定制。Docker的镜像是不可变的,而且前后层的关系是绑定的,后一层会明确指定所依赖的前一层的hash摘要,类似于区块链,形成一个不可被篡改的时间线。

相关推荐
安的列斯凯奇10 分钟前
分布式事务介绍 Seata架构与原理+部署TC服务 示例:黑马商城
分布式·架构
BinaryBardC1 小时前
Bash语言的数据类型
开发语言·后端·golang
Pandaconda1 小时前
【Golang 面试题】每日 3 题(二十一)
开发语言·笔记·后端·面试·职场和发展·golang·go
_院长大人_2 小时前
使用 Spring Boot 实现钉钉消息发送消息
spring boot·后端·钉钉
土豆凌凌七2 小时前
GO随想:GO的并发等待
开发语言·后端·golang
AI向前看3 小时前
C语言的数据结构
开发语言·后端·golang
SomeB1oody3 小时前
【Rust自学】10.8. 生命周期 Pt.4:方法定义中的生命周期标注与静态生命周期
开发语言·后端·rust
自律小仔4 小时前
Go语言的 的继承(Inheritance)核心知识
开发语言·后端·golang
爱在心里无人知4 小时前
Go语言的 的数据封装(Data Encapsulation)核心知识
开发语言·后端·golang
悟道茶一杯4 小时前
Go语言的 的注解(Annotations)核心知识
开发语言·后端·golang