From fd15cceb7eca8954f881ca77643cbe92abc7f039 Mon Sep 17 00:00:00 2001 From: Nop Assistant Date: Sat, 29 Jun 2024 12:42:48 +0000 Subject: [PATCH] docs: synced via GitHub Actions --- src/dev-guide/graphql/call-biz-model.md | 55 ++++ src/dev-guide/graphql/index.md | 3 + src/dev-guide/orm/orm-basic.md | 159 +++++++++--- src/theory/explanation-of-delta.md | 319 ++++++++++++++++++++++++ 4 files changed, 495 insertions(+), 41 deletions(-) create mode 100644 src/dev-guide/graphql/call-biz-model.md create mode 100644 src/theory/explanation-of-delta.md diff --git a/src/dev-guide/graphql/call-biz-model.md b/src/dev-guide/graphql/call-biz-model.md new file mode 100644 index 0000000..319c7f0 --- /dev/null +++ b/src/dev-guide/graphql/call-biz-model.md @@ -0,0 +1,55 @@ +# 跳过GraphQL引擎直接调用BizModel中的方法 + +BizModel中的服务方法可以通过NopGraphQL引擎对外暴露为服务函数,可以通过GraphQL、REST和gRPC等多种方式调用BizModel上的方法。 + +有的时候在后台集成使用的时候,可能会希望跳过NopGraphQL引擎,直接触发BizModel上的方法,比如说在Spring的工作流步骤中触发。 + +Nop平台提供了BizActionInvoker这个帮助类,通过它上面的方法来直接调用BizModel。 + +```java +public class BizActionInvoker { + /** + * 同步调用BizModel上的方法 + * @param bizObjName 业务对象名 + * @param bizAction 业务对象的服务方法名 + * @param request 请求对象,一般情况下为Map结构,包含所有发送给服务方法的参数。也可以是RequestBean对象 + * @param selection GraphQL执行时可选的结果字段选择机制。如果不需要选择,这里可以设置为null + * @param context 上下文对象。可以直接new ServiceContextImpl() + * @return 直接返回BizAction方法的返回结果,这里的结果没有经过GraphQL的dataLoader处理。 + */ + public static Object invokeActionSync(String bizObjName, String bizAction, Object request, + FieldSelectionBean selection, IServiceContext context) { + IBizObjectManager bizObjectManager = BeanContainer.getBeanByType(IBizObjectManager.class); + IBizObject bizObject = bizObjectManager.getBizObject(bizAction); + GraphQLOperationType opType = bizObject.getOperationType(bizAction); + IOrmTemplate ormTemplate = BeanContainer.getBeanByType(IOrmTemplate.class); + ITransactionTemplate txnTemplate = BeanContainer.getBeanByType(ITransactionTemplate.class); + + return ormTemplate.runInSession(session -> { + if (opType == GraphQLOperationType.query) { + Object ret = bizObject.invoke(bizAction, request, selection, context); + return FutureHelper.getResult(ret); + } else { + // 其他情况下假设都需要事务处理 + return txnTemplate.runInTransaction(null, TransactionPropagation.REQUIRED, txn -> { + Object ret = bizObject.invoke(bizAction, request, selection, context); + return FutureHelper.getResult(ret); + }); + } + }); + } + + /** + * 通过GraphQLEngine调用BizModel上的方法。它会捕获所有异常,返回ApiResponse对象。内部会自动打开事务环境和OrmSession环境,并自动实现事务回滚 + */ + public static ApiResponse invokeGraphQLSync(String bizObjName, String bizAction, + ApiRequest request) { + IGraphQLEngine graphQLEngine = BeanContainer.getBeanByType(IGraphQLEngine.class); + IGraphQLExecutionContext gqlCtx = graphQLEngine.newRpcContext(null, bizObjName, request); + return graphQLEngine.executeRpc(gqlCtx); + } +} +``` + +* invokeActionSync调用会直接触发BizObject上的action,它会转化为对BizModel上方法的调用。这种调用方式与GraphQL调用的区别在于不会对返回结果执行GraphQL Selection处理。 + diff --git a/src/dev-guide/graphql/index.md b/src/dev-guide/graphql/index.md index f76afff..14c01a3 100644 --- a/src/dev-guide/graphql/index.md +++ b/src/dev-guide/graphql/index.md @@ -28,5 +28,8 @@ 参见[fragments.md](fragments.md) +## 不经过GraphQL引擎直接调用BizModel +参见[call-biz-model.md](call-biz-model.md) + ## 关于GraphQL的答疑 参见[qa-about-graphql.md](qa-about-graphql.md) diff --git a/src/dev-guide/orm/orm-basic.md b/src/dev-guide/orm/orm-basic.md index d05fefc..bcd4242 100644 --- a/src/dev-guide/orm/orm-basic.md +++ b/src/dev-guide/orm/orm-basic.md @@ -24,9 +24,9 @@ query.addFilter(eq(PROP_Name_username,"张三")) eq(PROP_NAME_gender, 1) ))) .addOrderField(PROP_NAME_createTime, true); - -IEntityDao dao = daoProvider.daoFor(User.class); -List usreList = dao.findAllByQuery(query); + +IEntityDao dao = daoProvider.daoFor(User.class); +List usreList = dao.findAllByQuery(query); // 如果分页查询 query.setOffset(100); @@ -43,7 +43,7 @@ User user = dao.findFirstByQuery(query); User example = new User(); user.setStatus(10); -IEntityDao dao = daoProvider.daoFor(User.class); +IEntityDao dao = daoProvider.daoFor(User.class); List userList = dao.findAllByExample(example); User user = dao.findFirstbyExample(example); long count = dao.countByExample(example); @@ -98,13 +98,13 @@ Nop平台非常强调同一种模型信息存在多种表达形式,并且这 ```xml - - - - - - - + + + + + + + ``` @@ -117,7 +117,7 @@ bo.xlib中提供了对CrudBizModel中doFindPage等函数的封装 @SqlLibMapper("/app/mall/sql/LitemallGoods.sql-lib.xml") public interface LitemallGoodsMapper { - void syncCartProduct(@Name("product") LitemallGoodsProduct product); + void syncCartProduct(@Name("product") LitemallGoodsProduct product); } ``` @@ -126,20 +126,20 @@ public interface LitemallGoodsMapper { ```xml - - - - - - update LitemallCart o - set o.price = ${product.price}, - o.goodsName = ${product.goods.name}, - o.picUrl = ${product.url}, - o.goodsSn = ${product.goods.goodsSn} - where o.productId = ${product.id} - - - + + + + + + update LitemallCart o + set o.price = ${product.price}, + o.goodsName = ${product.goods.name}, + o.picUrl = ${product.url}, + o.goodsSn = ${product.goods.goodsSn} + where o.productId = ${product.id} + + + ``` @@ -154,17 +154,17 @@ public interface LitemallGoodsMapper { ```xml - - - - - - - - - - - + + + + + + + + + + + ``` @@ -173,8 +173,8 @@ public interface LitemallGoodsMapper { ```xml - - + + ``` @@ -204,7 +204,7 @@ public interface LitemallGoodsMapper { ```xml - and o.classId = ${myVar} + and o.classId = ${myVar} ``` @@ -218,7 +218,34 @@ NopGraphQL引擎执行时已经自动开启了OrmSession,所以一般的业务 注意使用Nop平台内定义的Transactional)注解, 它们会自动打开OrmSession和事务管理器。 -如果要手工打开session,可以采用如下方法 +```java +public class TccRecordRepository implements ITccRecordRepository { + // 这里强制设置开启新的事务,一般情况下设置propagation,会自动继承上下文中已有的事务 + @Transactional(propagation = TransactionPropagation.REQUIRES_NEW) + @Override + public CompletionStage saveTccRecordAsync(ITccRecord record, TccStatus initStatus) { + return FutureHelper.futureCall(() -> { + NopTccRecord tccRecord = (NopTccRecord) record; + tccRecord.setStatus(initStatus.getCode()); + recordDao().saveEntityDirectly(tccRecord); + return tccRecord; + }); + } + // ... +} +``` + +所有使用`@Transctional`这样的注解的bean,都需要在NopIoC的`beans.xml` +文件中注册。因为AOP是使用NopIoC的内置能力实现的。参见[aop.md](../ioc/aop.md) + +```xml + + + + +``` + +### 如果要手工打开session,可以采用如下方法 ```javascript @@ -241,3 +268,53 @@ transactionTemplate.runInTransaction(txn->{ ... }) ``` + +## 与MyBatis的区别 + +NopORM是一个类似JPA的完整的ORM引擎,因此它使用OrmSession来管理所有加载到内存中的实体,整体使用类似于JPA和Hibernate,相比于MyBatis要少很多手工调用步骤。 + +### 1. 修改的时候不需要调用update方法。 + +一般情况下我们是使用IEntityDao接口来实现实体的增删改查。它内部使用OrmTemplate来调用底层的NopORM引擎。 +OrmTemplate类似于Spring中的HibernateTemplate,调用它上面的方法时会自动打开OrmSession,并在操作完毕后调用`session.flush()` +来将内存中的修改刷新到数据库中。 + +因此从数据库中加载到实体之后,我们只需要调用set方法即可,不需要调用任何update方法,引擎会负责检测实体是否已经被修改,如果已经被修改,则自动更新数据库。 +更新数据库的时候与MyBatis不同,NopORM会自动根据修改了的字段生成对应的update语句,因此即使调用了set方法,但是如果实际并没有修改实体属性,则最后实体的状态不会转化为dirty,也就不会更新数据库。 + +```javascript +@SingleSession +@Transactional +public void changeEntityStatus(String id, int status){ + IEntityDao dao = daoProvider.daoFor(MyEntity.class); + MyEntity entity = dao.requireEntity(id); + entity.setStatus(3); + + // 这里不需要调用dao.updateEntity(entity); +} +``` + +如果是在BizModel的函数中调用,则不需要使用@SingleSession和@Transactional注解, NopGraphQL引擎会负责统一处理。 + +### 2. 新增的时候也不一定需要调用save方法 +只要把新增实体和OrmSession中已经存在的其他实体关联在一起,NopORM引擎flush的时候就会自动沿着对象关联遍历到该实体。如果发现该实体还没有保存,则会自动生成insert语句。 + +```javascript + MyEntity entity = dao.newEntity(); + entity.setName("ssS"); + parent.getChildren().add(entity); +``` + +### 3. 一般情况下不要调用updateDirectly这样的方法 +为了实现性能最大化,NopORM也提供了updateDirectly等绕过OrmSession直接生成SQL的更新方式。但是这相当于是一种性能后门,一般不要使用。 + +### 4. 尽量使用EQL而不是SQL +NopORM提供了类似MyBatis XML的SQL语句管理机制,在`sql-lib.xml`可以使用EQL、SQL和DQL等多种查询语法。 + +EQL类似于Hibernate中的HQL查询语言,可以使用`entity.parent.name`这种属性关联语法,但是EQL比HQL强大得多。在EQL中可以自由使用各种join语法, +with子句、limit子句、update returning子句等, + +* 从设计层面上说 `EQL = SQL + AutoJoin`,原则上一切SQL语言具有的语法EQL语法都支持,而且在此基础上, +EQL语法增加了根据属性关联自动推导得到表关联的特性。 +* 实际实现中,EQL语法支持大部分标准SQL92语法,但是它为了数据库兼容性,只支持多个主流数据库都具有的语法特性,不支持专属于某个数据库的专有语法。对于SQL函数,通过dialect配置实现了兼容转换。 +* EQL支持GIS相关的`st_contains`等函数 diff --git a/src/theory/explanation-of-delta.md b/src/theory/explanation-of-delta.md new file mode 100644 index 0000000..a72b0ce --- /dev/null +++ b/src/theory/explanation-of-delta.md @@ -0,0 +1,319 @@ +# 写给程序员的差量概念辨析,以Git和Docker为例 + +可逆计算理论提出了一个通用的软件构造公式 + +``` +App = Delta x-extends Generator +``` + +Nop平台的整个实现可以看作是对这个公式的一种具体落地实践。这其中,最关键也是最容易引起误解的部分是可逆计算理论中的差量概念,也就是上面公式中的Delta。 + +可逆计算可以看作是针对差量概念的一个系统化的完整理论,所以业内目前常见的基于差量概念的实践都可以在可逆计算理论的框架下得到诠释。我在举例时经常会提到git和docker,比如 + +> 问: 什么是Delta? 每一次git的commit是不是一个delta? +> +> 答:git是定义在通用的行空间上,所以它用于领域问题是不稳定的,领域上的等价调整会导致行空间中的合并冲突。类似的还有Docker技术。很多年以前就有python工具能管理依赖,动态生成虚拟机。这里本质性的差异就在于虚拟机的差量是定义在无结构的字节空间中,而docker是定义在结构化的文件空间中。 + +有些人读到上面的文字后感觉有点被绕晕了。"类似的还有Docker技术" 这句话到底是在说这两种本质上是一样的?还是想说它们本质上不一样? + +这里确实需要一些更详尽的解释。虽然都叫做差量,但是差量和差量之间仍然有着非常深刻的区别。总的来说git和docker本质上都涉及到差量计算,但是它们所对应的差量也有着本质上不同的地方,这里的精细的差异需要从数学的角度才能解释清楚。一般人对于差量概念的理解其实是望闻生义,存在着非常多的含混的地方,所以很多的争论本质上是因为定义不清导致的,而不是这个问题本身内在的矛盾导致的。 + +> 可逆计算理论对差量的理解和使用在数学层面上其实非常简单,只是它的应用对象不是一般人习以为常的数值或者数据,导致没有经过专门抽象训练的同学一时转不过来弯而已。 + +因为有些同学反映对于此前文章中提到的数学名词感觉懵懵懂懂,所以在本文中我会尝试补充一些更详细的概念定义和分析步骤。如果仍然有感觉不清楚的地方,欢迎加入Nop平台的讨论群继续提问,讨论群的二维码见微信公众号的菜单。 + +## 一. 差量的普适性和存在性 + +在数学和物理领域,当我们提出一个新的概念时,第一步就是论证它的普适性和存在性。所以可逆计算对于差量概念的第一个关键性理解是:**差量是普遍存在的**。 + +``` +A = 0 + A +``` + +任何一个存在单位元的系统都可以很自然的定义出差量:任何一个全量都等价于单位元+自身,也就是说任何一个全量都是差量的特例。而单位元这个概念在一般情况下是自然存在的,比如什么都不干的情况下对应于空操作,而空操作和任何其他操作结合在一起都等价于这个操作本身。 + +很多人听到**全量是差量的特例**这句话之后可能会感到很疑惑,这很显然,但是它有什么意义吗?一般人无法蒋数学原理和真实的物理世界联系起来,导致他们会认为数学的精确定义是无用的。这里有个很关键的推论是这样的:既然全量是差量的特例,那么原则上**全量可以采用和差量一模一样的形式**,没有必要为了表达差量,单独设计一个不同的差量形式。比如说,JSON格式的差量可以用JSON Patch语法来表达,但是JSON Patch的格式与原始的JSON格式有着很大的区别,它这个差量形式就是特制的,不是与全量格式保持一致的。 + +``` +[ + { + "op": "add", + "path": "/columns/-", + "value": { + "name": "newColumn", + "type": "string", // 或者其他数据类型 + "primary": false // 是否为主键 + } + }, + { + "op": "remove", + "path": "/columns/2" + } +] +``` + +Nop平台中的做法是: + +```xml + + + + + +
+``` + +或者使用JSON格式 + +``` +{ + "type": "table", + "name": "my_table", + "columns": [ + { + "type": "column", + "name": "newColumn", + "type": "string", + "primary": false + }, + { + "name": "column2", + "x:override": "remove" + } + ] +} +``` + +JSON Patch的格式与原始JSON的格式是完全不同的,而且只能使用下标来标记列表中的行,而Nop中的Delta合并是通过name等唯一属性来定义行,在语义层面更加稳定(插入一行不会影响到所有后续行的定位),而且差量的格式与原始格式完全一致,只是额外引入了一个`x:override`属性。 + +在Nop平台中我们系统化的贯彻了**全量是差量的一个特例**的数学思想,各种需要表达差量的地方,比如前台提交到后台的修改数据、后台提交到数据库的变更数据等,我们全部采用所谓的同构设计,即提交的差量数据结构和普通的领域对象结构基本一致,通过少量扩展字段来表达新增、删除等信息。 + +认识到全量是差量的一个特例之后,还可以破除一个常见的误解:差量是一些局部的小的变化,实际上差量完全可以大到这个系统。在存在逆元的情况下,**差量甚至可以比整体还要大!** + +## 二. 不同的空间有不同的差量定义 + +现代抽象数学所带来的一个关键性认知是:**运算规则是与某个空间绑定的**。因为我们在中学所学的数学规则都是应用在自然数、有理数、实数这种习以为常的数值空间中,所以大部分人没有意识到运算规则仅在它对应的空间中有效,而且在定义的时候,运算规则和它所作用的空间就是作为一个整体来定义的。在数学中,包含单位元和逆元概念的最小化的数学结构是群(Group),下面我们就仔细分析一下群的定义。 + +### 数学中的群(Group) + +智谱清言AI给出的标准定义如下: +一个群 $ (G, *) $ 由一个集合 $ G $ 和一个二元运算 $ *: G \times G \rightarrow G $ 组成,使得以下四个条件成立: + +1. **封闭性(Closure)**:对于所有 $ a, b \in G $,$ a * b $ 也在 $ G $ 中。 +2. **结合性(Associativity)**:对于所有 $ a, b, c \in G $,有 $ (a * b) * c = a * (b * c) $。 +3. **单位元(Identity Element)**:存在一个元素 $ e \in G $,对于所有 $ a \in G $,有 $ e * a = a * e = a $。这个元素 $ e $ 被称为群的单位元。 +4. **逆元(Inverse Elements)**:对于每个 $ a \in G $,存在一个元素 $ a^{-1} \in G $,使得 $ a * a^{-1} = a^{-1} * a = e $。这个元素 $ a^{-1} $ 被称为 $ a $ 的逆元。 + +首先我们注意到,在群的定义中,基础的集合G和它上面的运算`*`是一个整体,单独的G和单独的运算都无法构成群。但是在日常交流中,我们一般会将群$(G, *)$简称为群G,这可能会给一些人造成误导。 + +群定义中的`*`仅仅是一种抽象的符号标识,它并不代表乘法。在不同的空间中我们可以定义不同的运算,而在不同的运算中单位元和逆元的定义也是不同的。比如说,实数空间$\mathbb{R}$ 上的加法和乘法各自构成群,但它们不是同一个群的运算。下面是智谱清言AI给出的详细定义, + +1. **加法群** $(\mathbb{R}, +)$: + + - 封闭性:对于所有$a, b \in \mathbb{R}$,a + b 也在$\mathbb{R}$ 中。 + - 结合性:对于所有$a, b, c \in \mathbb{R}$,有$(a + b) + c = a + (b + c)$。 + - 单位元:存在一个元素$0 \in \mathbb{R}$,对于所有$a \in \mathbb{R}$,有$0 + a = a + 0 = a$。这个元素$0$ 是加法的单位元。 + - 逆元:对于每个$a \in \mathbb{R}$,存在一个元素$-a \in \mathbb{R}$,使得$a + (-a) = (-a) + a = 0$。这个元素$-a$ 是$a$ 的加法逆元。 + 因此,实数空间$\mathbb{R}$ 在加法运算下构成了一个群,称为加法群。 + +2. **乘法群** $(\mathbb{R}^*, \cdot$): + + > 注意,乘法群不包括零,因为零没有乘法逆元。因此,我们考虑的是非零实数构成的集合$\mathbb{R}^*$。 + + - 封闭性:对于所有$a, b \in \mathbb{R}^*$,$a \cdot b$ 也在$\mathbb{R}^*$ 中。 + - 结合性:对于所有$a, b, c \in \mathbb{R}^*$,有$(a \cdot b) \cdot c = a \cdot (b \cdot c)$。 + - 单位元:存在一个元素$1 \in \mathbb{R}^*$,对于所有$a \in \mathbb{R}^*$,有$1 \cdot a = a \cdot 1 = a$。这个元素$1$ 是乘法的单位元。 + - 逆元:对于每个$a \in \mathbb{R}^*$,存在一个元素$a^{-1} \in \mathbb{R}^*$,使得$a \cdot a^{-1} = a^{-1} \cdot a = 1$。这个元素$a^{-1}$ 是$a$ 的乘法逆元。 + 因此,非零实数构成的集合$\mathbb{R}^*$ 在乘法运算下构成了一个群,称为乘法群。 + +可逆计算理论中的差量概念,本质上是来源于群的思想的启发,因此对于每一种差量,我们总是可以仿照群的定义,从以下的几个方面进行分析: + +1. 差量运算是在哪个空间中定义的? + +2. 差量运算的结果是否仍然在这个空间中? + +3. 差量运算是否满足结合律?能否先执行局部的运算,然后再和整体进行结合? + +4. 差量运算的单位元是什么? + +5. 差量运算是否支持逆运算?逆元的形式是什么? + +## 三. Git中的差量运算 + +### 1. Git的差量运算是在哪个空间中定义的? + +Git的diff功能是把文本文件先拆分成行,然后再对文本行的列表进行比较。因此,Git的差量结构空间可以看作是行文本空间。这是一个通用的差量结构空间。每一个文本文件都可以映射到行文本空间中成为一个行文本列表,换句话说,每一个文本文件在行文本空间中都有一个属于自己的表象(Representation)。 + +### 2. Git的差量运算的结果是否仍然在行文本空间中? + +Git的apply功能可以将patch差量文件应用到当前的文本文件之上,得到的仍然是一个”合法的“文本文件。但是如果细究起来,这里的合法性并不是那么牢靠。 + +首先,Git中的patch具有很强的特异性,它与自己的应用目标是紧密捆绑在一起的。比如说,从项目A构造出来的patch无法直接应用到另外一个不相关的项目B上。一个patch就是针对一个指定版本(状态)的base文件。在这种情况下,我们很难认为patch在概念层面是一种独立的实体,存在独立的价值。 + +第二,多个patch应用到同一个base文件之上时可能会出现冲突,在冲突的文件中我们会改变文件原始的结构,插入如下标记内容: + +- `<<<<<<< HEAD`:标记当前分支(通常是你的目标分支,即HEAD所指向的分支)的内容开始。 +- `=======`:分隔当前分支和被合并分支的内容。 +- `>>>>>>> [分支名]`:标记被合并分支(通常是你要合并的分支)的内容开始。 + +**出现冲突的本质原因是差量运算超出了原定的结构空间,产生了某种异常结构**。这种结构并不在原先合法结构的定义范围之内。 + +第三,即使多个patch没有发生冲突,合并后的结果也可能破坏源文件应有的语法结构。导致合并的结果虽然是一个”合法的“的行文本文件,但是它却不是一个合法的源码文件。**为了保证合并结果一定具有合法的语法结构,我们必然是需要在抽象语法树的结构空间中进行合并**。 + +在群定义的四大天条中,排在第一条的就是所谓的封闭性,这无疑是在暗示着它无可辩驳的重要性。但是一般没有受过抽象数学训练的同学却经常会忽略这一点。封闭性很重要吗?不封闭会死吗?数学的力量来自于连续的自动化推理,如果在推理的过程中随时可能突破到已知空间之外,进入某种未知状态,那就意味着数学推理的大厦随时可能会发生坍塌,唯一的指望只能是祈祷幸运女神的伴随和祈求上帝的救赎(程序员手工编辑冲突内容类似于上帝之手的介入)。 + +### 3. Git中的差量运算是否满足结合律? + +如果我们手里有多个patch文件,patch1、patch2、patch3...,这些小的patch文件能否被合并成一个大的patch文件?如果可以合并,得到的结果与合并的顺序有关吗? (patch1 + patch2) + patch3与 patch1 + (patch2 + patch3)的结果是否等价? + +如果Git的差量满足结合律,那就意味着我们可以在独立于base文件的情况下实现patch文件的合并,比如说通过如下指令实现合并patch。 + +``` +git merge-diff patch1.patch,patch2.patch > combined.patch +``` + +但很可惜,实际情况是,以上指令并不存在。Git合并多个patch时,必须逐个将patch应用到base文件上,然后再反向生成diff。 + +``` +git apply --3way patch1.patch +git apply --3way patch2.patch +git diff > combined.patch +``` + +结合律是数学领域中一条特别基本的普适规律,现有的各种主要数学理论全部预设了结合律的存在(包括号称最纯粹、最泛化的范畴论)。在数学的世界中,没有结合律几乎是寸步难行。 + +> 我所知道的唯一的不满足结合律的数学对象是八元数(octonions)。八元数是四元数(quaternions)的扩展,而四元数是复数的扩展。八元数目前只有一些很小众的应用。 + +结合律为什么重要?首先,**结合律使得我们可以实现局域化的认知**。在存在结合律的情况下,我们不需要考虑本体的存在,**不需要考虑离我们很远的推理链条中发生的事情,只要将全部精力放到直接与自己发生相互作用的对象上就好了**。只要研究清楚了一个对象可以和哪些元素结合,它们之间发生结合运算之后产生的结果是什么,那么在范畴论的意义上就是彻底掌握了关于这个对象的一切知识(在马克思主义哲学中,这对应于人是他所参与的一切生产关系的总和)。 + +第二,**结合律使得复用成为可能**。我们可以将相邻的几个对象预先结合在一起,形成一个完整的、具有独立语义的新的元素。 + +``` +x = a + b + c = a + (b+c) = a + bc +y = m + b + c = m + bc +``` + +在上面的例子中,在构造x和y的过程中我们可以复用预结合产生的对象bc。但是很有趣的是,如果复用真的能够发生作用,那么要求同样的对象可以和很多对象结合。比如 `a+bc`和`m+bc`。但是,**在Git的差量运算中,patch没有这样的自由可结合性,它只能应用于固定版本的base文件,因此基本上可以认为它不具备可复用性**。 + +``` +... + a + b + c + ... + == ... + (a + (b + c)) + ... + == ... + ((a + b) + c) + ... +``` + +满足结合律意味着可以自由的选择是否与临近的对象结合,决定先进行左结合还是先进行右结合,亦或是不结合等着别人主动来结合。在形式上,这意味着我们可以随时在计算序列中插入或者删除括号,计算的顺序不影响最终得到的结果。因此,结合律的第三个作用是,**为性能优化创造了可能性**。 + +``` +function f(x){ + return g(h(x)) +} +``` + +函数运算满足结合律,因此编译期在编译的时候可以直接分析函数`g`和`h`的代码,抽取出它们的实现指令,然后在完全不了解函数`f`的调用环境的情况下,对`g`和`h`的指令进行合并优化。此外,结合律也为并行优化创造了可能性。 + +``` +a + b + c + d = (a + b) + (c + d) = ab + cd +``` + +在上面的例子中,我们可以同时计算`a+b`和`c+d`。很多快速算法都依赖于结合律所提供的这种优化可能性。 + +### 4. Git的差量运算的单位元和逆元是什么? + +Git的差量运算的单位元显然就是一个空的patch文件,它表示什么都不做。有些同学可能会感到奇怪,既然单位元什么都不做了,那它还有什么存在的必要性吗?首先,我们需要了解一下单位元的特殊性:**单位元一旦存在,它就会无处不在**。 + +$$ +a*b = e*e*e*a*e*e*b*e*e*e +$$ + +在任何对象的前后都可以插入任意数量的单位元。这意味着表面上看起来a和b是直接发生相互作用,实际上它们是**浸泡在单位元的海洋中,间接发生相互作用的**。so what,这个单位元海洋能闹出啥动静吗?要真正理解单位元的重要性,还需要结合着逆元的存在性来看。 + +$$ +e = a*a^{-1} = a^{-1}*a +$$ + +现在单位元海洋就不是空无一物了,它提供无限多种中间运算过程,只是最后的计算结果是归于虚无而已。 + +$$ +a*b = a *e * b = a * c*d * d^{-1} * c^{-1} * b +$$ + +假设现在我们已经构造好了$a*c*d$,则我们可以复用这个构造结果来构造$a*b$ + +$$ +acd * d^{-1} * c^{-1} * b = ab +$$ + +> 在我们所处的物理世界中,在人力所不能及的量子虚空中,表面上看起来空空如也,实际上只是正粒子和反粒子相互竞争达成了某种动态平衡。此时如果附近存在一个黑洞,由于黑洞的引力场很强,可能会导致随机涨落创生的正反粒子被拉开,最终其中一个落入黑洞视界,另外一个逃离黑洞,产生传说中的霍金辐射,黑洞蒸发现象。 + +在可逆计算理论中,这一点正是实现粗粒度复用的关键所在。 + +``` +X = A + B + C +Y = A + B + D + = A + B + C + (-C) + D + = X + (-C+D) = X + Delta +``` + +假设X由`A+B+C`构成,我们现在想生产`A + B +D`所组成的Y,如果存在逆元和单位元,则我们可以从X出发,**在完全不拆解X的前提下**,通过一系列的差量运算将X转换为Y。利用结合律,我们可以将`-C`和D聚集在一起,形成一个完整、独立的Delta差量。在可逆计算的视角下,软件复用的原理发生了本质性的变化:从组件复用的**相同可复用**转换到可逆计算的**相关可复用**:任意的Y和任意的X之间都可以通过Delta建立转换关系(Transformation),从而形成复用,而不需要它们之间构成传统的部分-整体这样的组合关系(Compostion)。 + +逆元和单位元对于解方程这种复杂的推理模式也是必不可少的。 + +``` +A = B + C +-B + A = -B + B + C = 0 + C +C = -B + A +``` + +解方程时之所以可以移项,本质上是在方程两侧都加上逆元,然后再省略生成的单位元。 + +Git可以反向应用patch,也可以利用patch指令来生成反向patch + +``` +git apply -R original.diff + +patch -R -o reversed.diff < original.diff +``` + +但是因为没有结合律,Git中对于反向patch的应用也就乏善可陈了。 + +根据本节的分析,**Git虽然提供了某种差量运算,但是它的数学性质却很难让人满意,这也就意味着基于Git的差量很难进行大规模的自动化处理,随时都会因为差量运算失效而导致需要人工介入**。但是与Git相比,Docker的情况就要好很多,它的差量运算堪称完美。 + +## 四. Docker中的差量运算 + +### 1. Docker的差量运算是在哪个空间中定义的? + +Docker所依赖的核心技术之一是所谓的堆叠文件系统OverlayFS,它依赖并建立在其它的文件系统之上(例如 ext4fs 和 xfs 等等),并不直接参与磁盘空间结构的划分,**仅仅将原来底层文件系统中不同的目录进行 “合并”,然后向用户呈现,这也就是联合挂载技术**。OverlayFS在查找文件的时候会先在上层找,找不到的情况下再到下层找。如果需要列举文件夹内的所有文件,则会合并上层目录和下层目录的所有文件统一返回。如果用Java语言实现一种类似OverlayFS的虚拟文件系统,结果代码就类似于[Nop平台中的DeltaResourceStore](https://gitee.com/canonical-entropy/nop-entropy/blob/master/nop-core/src/main/java/io/nop/core/resource/store/DeltaResourceStore.java)。OverlayFS的这种合并过程就是一种标准的树状结构差量合并过程,特别是我们可以通过增加一个Whiteout文件来表示删除一个文件或者目录。 + +**Docker镜像的Delta差量是定义在文件系统空间中,所谓的Delta的最小单位不是字节而是文件**。比如说,如果我们现在有一个10M的文件,如果我们为这个文件增加一个字节,则镜像会增大10M,因为OverlayFS要经历一个[copy up](https://blog.csdn.net/qq_15770331/article/details/96702613)过程,将下层的整个文件拷贝到上层,然后再在上层进行修改。 + +有些人可能会认为Git和Docker的差量的区别在于一个是线性列表,一个是树形结构。但是这并不是两者之间的本质性差异。真正重要的区别是**Docker的差量结构空间中存在着可以用于唯一定位的稳定坐标**:每个文件的完整文件路径可以看作是在文件结构空间中定位的唯一坐标。这个坐标或者说所有坐标组成的坐标系之所以被称为是稳定的,是因为当我们对文件系统进行局部改变时,比如新增一个文件或者删除一个文件,不会影响到其他文件的坐标。而Git中不同,它是使用行号来作为定位坐标的,因此只要新增行或者删除行,就会产生大量后续行的坐标变动,因此Git所提供的坐标系是不稳定的。 + +Docker的坐标系只管理到文件级别,如果我们想在文件内部进行唯一定位并实现差量计算需要怎么办?Nop平台通过XDef元模型在DSL领域模型文件内部建立了领域坐标系,可以精确的定位到XML或者JSON文件中的任意节点。除此之外,Nop平台还内置了一个类似OverlayFS的虚拟文件系统,它将多个Delta层堆叠为一个统一的DeltaFileSystem。 + +是不是一定要采用树形结构空间?也不一定。比如说,AOP技术所应用的结构空间可以看作是`包-类-方法`这样一个固定的三层结构空间,而不是支持任意深度嵌套的树形结构空间。类似的,在关系数据库领域,我们使用的是`表-行-列`这样一种三层结构空间。只要定位坐标是稳定的,我们都可以基于它们发展一些系统化的差量运算机制。 + +> Tree结构具有很多优点。首先,它**实现了相对坐标与绝对坐标的统一**:从根节点开始到达任意节点只存在唯一的一条路径,它可以作为节点的绝对坐标,而另一方面,在某一个子树范围内,每一个节点都具有一个子树内的唯一路径,可以作为节点在子树内的相对坐标。根据节点的相对坐标和子树根节点的绝对坐标,我们可以很容易的计算得到节点的绝对坐标(直接拼接在一起就可以了)。 +> +> Tree结构的另一个优点是**便于管控,每一个父节点都可以作为一个管控节点**,可以将一些共享属性和操作自动向下传播到每个子节点。 + + + +## 五. 同一个物理变化可以投射到不同的表示空间 + +当我们修改了一个函数,这件事情实际上是同时被投射到了多个表示空间。 + +## 六. 总结 + +世界的本体是不可观测的。物理学格物以致知,我们所能感受到的不过是深不可测的世界本体之上被激发的一丝涟漪(差量)而已。 + +要深入的理解差量概念,需要从数学中群结构定义出发:封闭性、结合性、单位元、逆元。这其中,逆元是一个非常关键性的概念。在软件领域,函数式编程炒热了Monad一词,它基本是满足群结构定义四大天条中的前三条,可以看作是幺半群结构,缺少逆元的概念。 + +> 满足封闭性和结合性称为半群,在此基础上增加单位元,则称为幺半群。 + +可逆计算理论明确指出了逆元概念在软件构造领域的重要性,并结合产生式编程,提出了一个系统化的实施差量计算的技术路线 + +``` +App = Delta x-extends Generator +``` + +在可逆计算理论的指导下,我们有必要重新思考软件的构造基础,基于差量的概念重建我们对于底层软件结构的理解。**在5到10年内,我们可以期待整个业界发生一次从全量到差量的概念范式转换,我愿将它称之为差量革命**。