跳到主要内容

Nop 平台理论探讨

为避免隐私泄漏,群友问答均以匿名形式组织。另外,为便于阅读和理解,部分问答内容会做相应调整。

要点整理

Unison 是算是一种实现技巧吧,根据 content 得到 hash,从而得到一种类似名称, 但是又能指向结构的东西,本身与版本号类似,只是它基于结构本身, 而不是名称来确定唯一性而已。当我们想消除思维中的含混性的时候, 最简单的做法就是进入相空间描述,将结构在时间轴上的每个点都区分出来, 但是这样会大大增加整体描述的复杂性。

在现有的技术条件下 GraphQL 是一个很好的选择。 本质上是信息分布的改变,原先信息由服务端完全确定, 现在交互信息是服务端和客户端共同决定的, 一部分信息(具体使用哪些字段)这些信息放到了客户端, 也就是说要改变原有信息的组织方式,把 A 和 B 的某种协商作为交互的一部分。

原则上说每个服务(这里指微服务)都不是直接对接的,而是经过某种环境对象交互来发生相互作用的。 所以真正的作用是 A + B + 环境,环境本身可以提供柔性适配的功能, 可以注入外部公共知识、公共规则等。

如果要以演化的、松耦合的方式来看待外部世界,那意味着任何时候你看见的都不是对象的全部, 你只知道你需要知道的信息,而你需要知道别的信息的时候可以看到, 你也可以选择以旧的方式继续看待原有的信息。

Delta 需要一个预定义的坐标系作为参照物,Delta 只有在同一个坐标系下才能进行合并。 如果我们认为 Delta 是针对整个系统的,就会导致预定义的坐标系过于复杂。

实际上需要将整个系统分解为多个 DSL,然后一个 Delta 包中包含多个 DSL Delta。

可逆计算理论有没有可能支持运行期的演化,在用户使用过程中不断完成自我进化

这是一个有趣的问题。先说一下结论:可以,可逆计算可以应用于运行期演化

比如现在 Nop 平台中的设计实际上是延迟编译、即时编译的,如果上线之后我们修改 DSL 模型, 则所有依赖于这个模型的所有模型都会自动失效,下次再访问到这些模型的时候会自动重新加载编译。 例如,如果 NopAuthUser.xmeta 被修改了,则 NopAuthUser.view.xml 和用到 NopAuthUser.view.xml 的所有页面模型也会自动更新。在整个过程中是不需要重新启动系统的, 都是在运行期重新编译即可。SaaS 多租户系统在抽象的层面上看,也可以理解为一个 Delta 定制问题, 每个租户相当于对应于一个 Delta,而且这些 Delta 在运行时的状态空间也是相互隔离的。 实际上,因为 Nop 平台系统化的考虑了 Delta 的构建、分解机制, 相比于其他技术方案它能够更加优雅的处理运行期演化问题。 基于 Nop 平台的设计模式,我们可以实现完全不停机的情况下持续的软件演化。

更为复杂的一个问题是运行期的状态空间是否也能纳入 Delta 的管理范围, 毕竟整个应用系统的完整描述 = 结构 + 状态。答案同样是可以,因为可逆计算是一个完全抽象的理论, 它可以将结构空间 + 状态空间综合在一起,定义一个完整的高维空间, 然后再考虑这个高维空间中的差量化演化(在物理学中,这个高维空间被称为相空间)。 但是从实践的角度上说,考虑状态将导致需要处理的复杂度直线上升, 所以一般情况下我们只会考虑结构空间中的 Delta,而忽略状态空间。

具体举一个例子,比如说我们现在要对一个机器人进行 Delta 改进。 对这个机器人的完整描述肯定是包括它的结构,同时还要包括这些结构所处的状态, 比如说它处于高速的运动状态中,我们需要描述它的各个部分的相对速度、加速度、角速度,甚至压力、温度等, 但是我们一般情况下不会在机器人的工作模式下对它进行改装,而是要把它转入到静息模式下,也就是某种非激活的模式, 工作模式下的各种信息对于改装而言是不相关的、可忽略的。少数极端的情况下,我们需要在行驶的火车上换车轮, 那么基本的做法大概是:先使用时间静止技术,在局部冻结时间线,然后将部分需要保持的状态序列化到存储中, 然后修改结构,再次加载状态,一切就绪后再恢复时间演化。 在 Delta 修正所应用的过程中,时间是静止的,各类状态是冻结的,状态本身也是可以进行 Delta 修正的数据。 比如说我们使用了某种锁机制,直接阻止外部操作,从外部使用者来看就是时间静止。 更复杂的技术涉及到快照复制,然后在平行宇宙中不断复制 Delta 进行追账,直到对齐到某个时间点,在短时间内静止时间, 完成时间线切换。关于时间静止的一个有趣应用, 可以参见文章Paxos 的魔法学研究报告

对状态进行 Delta 修正的复杂度在于它并不是仅仅由对象自身决定的, 比如我们的机器人正在和别的机器人进行激烈的战斗,它的状态本质上是与对手相互作用导致的结果, 我们如果需要复现这个状态,必须要考虑到能量守恒、动量守恒、角动量守恒等一系列物理约束, 我们无法单方面的从机器人个体的角度出发复现它的状态,而是必须要考虑所有与它交互的客体的情况, 需要考虑环境与机器人之间的相互影响,这导致复现状态或者修改状态是一件非常复杂的事情。

根据上面的讨论,我们如果希望在运行期实现演化,基本的做法有两种:

  1. 分离结构和状态,比如,微服务本身就是结构而已,它本身不包含业务状态,所以可以随时启停;
  2. 定义激活和非激活两种模式,切换到非激活模式下完成结构修订;

如果发散一下考虑生物界的演化问题。我们可以把 DNA 看作是某种承载信息的 DSL, 生物的成长过程对应于 Generator,它根据 DSL 的信息并结合环境信息塑造出生物的本体。 同时生物在应对具体问题挑战的时候,还可以利用外部的各种 Delta。 比如人可以穿上潜水服这个 Delta,获得在水中活动的能力,可以加上不同厚度的衣服, 获得在极寒和极热地区活动的能力。而一般的动植物能够加装的 Delta 很少, 比如飞鸟,如果它在水中长时间潜水,飞行用的翅膀反而是一种阻碍。

那针对函数(业务处理逻辑)有可能采用差量机制么?如果可以,那是采用怎样的实现方案,又会面临哪些挑战呢?

函数应该就是 Generator

可逆计算的计算模式 App = Tree x-extends Tree<Tree>Generator 作为 AST 抽象语法树也是一个 Tree 结构。 所以业务逻辑只要领域化之后,也是可以定义有意义的差量修正。 Nop 平台中可以通过 task 或者 workflow 抽象来进行。 另外在平时实现代码生成器的时候我们会使用 xpl 标签库,标签库中的每个标签函数可以被单独定制。

目前我还在处理规则引擎。业务规则如果用 XML 来表达,可以是:

<rule>
<predicate>
<eq name="status" value="1"/>
<in name="type" value="${[1,2,3]}"/>
</predicate>

<outputs>
<output name="bizType" value="1"/>
</outputs>
</rule>

规则引擎参见 /nop/schema/rule.xdef

在系统采用差量机制后,那使用版本机制还有意义么?

Git 的版本相当于是一个比较粗粒度的非结构化差量, 可以给一次大的更新起一个名字,可以处理一般的 java 源码。 而 Delta 我们希望它是高度结构化、语义化的,如果所有的语言包括 Java 都实现合适的 Delta 差量化,则可以起到类似 Git 的作用。

据说 SAP 软件的更新包就是 Delta 化的,可以在不停机的情况下进行代码结构更新。 因为它所有的业务逻辑都是用自己的脚本语言实现的,而状态完全保存在数据库中, 脚本语言的加载器显然可以在加载的时候做很多替换工作。

那理想系统的整体工作量还很大,还需要实现一门 Delta 语言

Delta 只是一个特性,只需要在原语言的基础上加部分特性即可。 其实可以在 AspectJ 的基础上做简化就可以实现类-函数级别的 Delta。 C# 语言中的 PartiaICIass 也是一种弱化的 Delta。

对 Delta 的管理也需要一种基线之类的版本机制吧?或者单独对其使用 Git?

是的,可以用 Git 去管理 Delta。因为很多时候我们就想直接改 Delta 的实现,不需要保留语义边界。

xdef.xdef 中的 <meta:unknow-tag...> 为啥是代表任意名称的节点,不是应该就是指定名字是 unknow-tag 的节点吗?

xdef.xdefmeta 名字空间对应 xdef,而 xdef:unknown-tag 是平台解析时识别的节点。

<meta:unknown-tag> 在解析时是不会被识别解析的,但是为什么说是代表任意名称的节点呢?

unknown-tag 就是约定表示任意节点。

如果是这么约定的话,那 xdsl.xdef 文件,开头为啥非要用 xdef:unknow-tag 作为根节点,不是可以随便 abc

xdsl.xdef 表达的是所有 xdsl 领域语言所具有的共性。 wf/beans/sql-lib 这一系列的 xdef 所描述的 dsl 都是 xdsl, 除了它们各自具有的 xdef 元模型定义,它们还同时具有 xdsl.xdef 中描述的共性特征。所以 xdsl.xdef 中的节点匹配任意节点。

可不可以这么理解,xdef 文件是自定义出来的一种 dsl 语言,并不是严格意义上的 xml,所以不能按照 xml 的思维去理解。比如,api.xdef 里面,直接出现 xdef:xxxx 这样的属性,但是并没有严格声明 xdef 的名称空间。

是的。Nop 平台中的 xml 是使用 XNodeParser 解析的,并没有采用 Java 标准中的 xml 解析器。XNode 对于名字空间进行了简化设计,只考虑根节点上的名字空间,而且不校验内部节点的名字空间。 只有 xdef 的根节点上指定了 xdef:check-ns 的时候才检查这些指定名字空间的属性是否在 xdef 元模型中有定义。

那如果用 XNodeParser 去解析标准的 xml,遇上属性名称或者节点名称不在某个名称空间里面,也是校验不出来的是吧。

比如 a 命名空间下,只有 x 这个节点,然后 <a:y /> 是能过的。

Nop 平台按照可逆计算理论,它的设计永远是 data + metadata 配对设计, 也就是任何数据的临近之处总是设计有存放扩展信息的地方,因此 xdsl 语法中规定, 除非特殊指定需要校验名字空间,所有具有名字空间的属性和节点是不参与校验的。 这样就可以实现自由扩展,可以在不改变原先模型定义的情况下随时增加 Delta 模型定义。 如果需要检查 Delta 定义满足指定格式,可以自己写一个 xdef,使用 x:extends 从原有的 xdef 继承,在自己使用的 xdsl 文件中指定使用自己的 xdef 即可。 平台内置的解析器可以根据指定的新的 xdef 来解析 xml 文件,不需要执行代码生成。

使用 Nop 平台开发的所有功能,包括各类设计器、内部模型,都可以通过 Delta 定制进行扩展。 比如在平台内置的工作流设计器中增加配置属性或者删除已有的配置数据, 都可以通过在 Delta 目录下增加 Delta 定制文件来实现。

备注

也就是,ui:showi18n-en:displayName 等都属于扩展信息,且本质上也是 Delta,用于附加其他领域空间的配置信息。

那要解析并校验一个外来的 xml,人家本来就有标准 xml 那套 xsdl 名称空间定义,用 XNodeParser 去解析校验,还要自己重新造一遍轮子? 写一遍这个 xml 的模型定义 xdef 文件?

当然可能只要造一遍通用的轮子,按说的扩展机制,造一个解析 xsdl 转成 xdef 的扩展出来,再用生成的 xdef 去校验?

XNodeParser 并不会识别 xdef 名字空间,它只是把 xml 解析为 XNode。 一般的 xml 都可以解析,只是不会校验名字空间。DslModelLoader 之类的才会识别 x:schema,这种情况下,原则上 dsl 文件需要回避 xdslxdef 等内部名字空间, 但是也设计了冲突回避方式,就是使用 xmlns:xx="/nop/schema/xdef.xdef" 这种方式来重命名名字空间,比如 xdef.xdef 为了避免冲突将 xdef 重命名为了 xmeta。 但是这种重命名目前没有仔细检查,除了已经使用的少数场景,其他情况下有写死根据名字空间来处理的情况。

通过 Delta 可以管理产品主线和客户定制化的关系,与现在的 Git 版本管理不是一样的吗?

产品线难管是因为分支太多,膨胀了,没办法管了。

至于说的 Delta,可以认为 Git 产生的源码 diff,就是一种 Delta,只不过是跟目标环境语言绑死了而已。

不一样。Git 不是面向领域的,所以导致它的冲突概率大得多,多个分支之间的 Delta 定制方向也没有明确定义,导致意图丢失。比如一行中有两个属性修改,Git 就无法自动合并,而 Delta 定制永远都是可以自动合并的,而且通过 x:override 可以保持意图。本质上是信息空间中的表达和保持信息守恒的问题。

a 分支要 override 成 a,b 分支要 override 成 b,那还是冲突啊。

如果要按顺序叠加,base <- a <- b,这是线性应用的,多层应用按照执行顺序会自动解决冲突问题。 了解一下 trait 为什么可以避免面向对象中多重继承导致的概念冲突问题就明白了。

  1. 首先使用自定义结构空间而不是预设的通用行空间、类空间;
  2. DSL 相当于定义了一种领域特定的坐标系,在这个坐标系中可以精确定义定点的 Delta 变化。 可以类比物理中的狄拉克 Delta 函数去理解;

按顺序叠加这块明白了,冲突是没了,这样叠加完之后,得到的结果其实并不是用户最终要的,还得再叠加自己的东西上去。

本来就是这样。Delta_a + Delta_b 并不一定满足要求,这里可以通过增加额外的 Delta_c 来解决冲突问题。在 Feature Oriented Programming 的语境中 Batory 发明了一个所谓概念叫做 quark 来表达对这种冲突的识别和处理。

但是这种差异的识别也很难,顾客看到了一个产品的两个差异版本,他是不知道底层是多少个 Delta 组成的,他只会提出,需要这个版本里面的 xxxx,然后那个版本里面 yyy,合在一起就好了。

但是 xxxx,和 yyy 到底又有多少 Delta 组成的,这个其实最终跟分析源代码也没啥两样。

这样要求 Delta 满足交换律,本身如果的 Delta 不冲突,那么它们自然是满足交换律的, 否则的话 a + bb + a 是不同的,本身就需要额外再增加 Delta 来补齐。

其实产品线合并的痛点在这里。

产品线合并的痛点在于没法将产品化的部分和定制化的部分分离。可以分成 platform/product/deploy 这几个 Delta,然后对平台的修正和对产品的修正就可以独立于的部署修正隔离开来。 这些本身也需要一定的规划,不是随便乱来的。

其实怕的就是最后一层 deploy 的那个真的是五花八门,合起来必然有冲突。

可逆计算的观念是通过最外层的 Delta 来吸收偶然性需求,保护基础框架和 Base Delta 的稳定。 无法控制熵增也可以控制熵增的地方,这才是关键。 Delta 合并不会出现形式冲突,合并后必然满足 xdef 元模型要求,可以执行语义校验。

首先应用大了还是会分不同的业务模块,领域模型。 然后差量并不是用来组装不同业务功能点最终合成一个大产品, 而是对某一个业务模块的不同层次的差量定制。 最常用的应该就一个基础版本加一层差量,能做的扩展性就已经非常大了。

要是延续之前做法,确实不好。业务域也需要分割好。然后一步一步组装起来。

是的,差量不是用来进行分模块逻辑组织的。 就好像不要使用继承来进行分模块逻辑组织。 Delta 是用来解决可扩展性问题,解决含时演化的。

Delta 定制解决的问题是系统已经打包发布之后,无需修改原有代码,可以随意定制已有的所有功能。 所以平台的二次开发是无需修改平台代码的。 目前没有其他技术具有这个能力,它解决的问题也是传统技术所无法解决的问题。

Nop 平台目前就提供了粗粒度软件复用的机制,可以在不修改一个非常大的 X 的基础上,通过补充 Delta,将它转化成 Y。而所谓的可扩展性,本质上就是不修改 X,通过补充新的信息来达到生产新的功能的目的。

可逆计算主要涉及到的是编译期形式变换,运行时形式封装只是一个次要的问题。

DSL 最终要被生成成特定的语言,这样,Detla 定制就绕不开语言?

就好比现在的 Nop 平台是 Java 实现的,当需要做一个 Detla 叠加时, 好像没办法用 Python 或者其他语言就 override 或者修正某一部分 Delta。 尤其在外层修改原有功能时,必须还是要去考虑原有层的代码。

并不是这样。Nop 平台的做法相当于是标准化 Delta 空间。传统上的 Delta 都是针对特定业务领域、特定框架设计的,比如 Spring 中有 parentabstract 等属性,需要解析得到 BeanDefinition 对象然后处理,GraphQL 有 extendtype 等扩展定义,需要解析得到 GraphQLObjectDefinition 等对象后进行处理。 但是可逆计算理论是指出在对象层之前存在统一的、厚重的结构层, 恰如各式各样不同的建筑风格最终都使用统一的结构力学来构建。 所以本质上 Nop 平台中的做法是跨语言、跨框架的,它通过统一的 XNode 接口来规范化结构层,然后定义了 XNode 和 XML、JSON 等通用结构的双向映射关系,可以自动实现可逆变换,然后再逐步定义 JSON 和 Java 之间,JSON 和 Excel 之间,JSON 和数据库之间的双向映射。

关键的关键是无需考虑任何运行时框架的语义,在纯粹的 XNode 结构层面(形式层面)就可以完成所有可逆计算所要求的 Delta 运算。 无论是工作流引擎、报表引擎、ORM 引擎等都是统一使用同样的 XNode 结构。

其实就是全部面向 DSL 开发,Generator 就是虚拟机。

Nop 平台整体解决问题是两阶段方式:

  1. 定义领域专用的 DSL;
  2. 用 DSL 去承载业务;

DSL 可以在 XML/JSON/Excel/数据库/可视化设计器 等多种表示形式之间自由转换。 也可以通过 Generator 最终转换为某种程序代码。 但是这种生成是业务无关的,可以脱离业务一次性完成。

如果学习过物理学中的张量概念就会知道,理论的概念是唯一的, 但是它可以投射到不同的具体的坐标系中产生不同的表象,而不同表象之间可以自由转换。 所以 Nop 平台的做法很简单, 就是存在信息的某种类似张量的与具体坐标系无关的(与语言、框架、运行时无关)表示, 然后再定义它到具体的形式之间的表象(相当于是投射到某种坐标系中,一种语言就定义了一种坐标系)。

这些都是物理学中的概念,可以把上面的文章中关于 Talyer 级数的直觉解释仔细看一下。 努力的方向是,信息可以跨形式自由转换,而不是一旦写成代码就禁锢在某种框架、某种语言、某种运行时中。

物理学的定律是所谓坐标系无关的,这是爱因斯坦所提出的最重要的物理思想。 定律的张量形式不随坐标系变换而发生变化。但是在使用具体的坐标表达时,物理学的公式形式是在不断变化的, 但是一旦整理成张量形式,给张量指定一个抽象的符号,那么无论怎么变换这个符号都是不变的。

物理学中的张量与深度学习中的张量概念在内涵上是不一样的,不是同一个东西,但是有关联。

物理学中所有的基本概念都是坐标系无关的,但是所有具体问题的表述都要依赖坐标系, 那怎么能说物理定律与坐标系无关呢?这里的无关真正的含义是同样的物理定律在不同的坐标系中具有不同的形式, 恰如同样的业务在不同的框架、不同的语言中表达式也具有不同的形式,但是这些不同的形式背后存在统一的抽象内容, 可以使用张量概念进行统一描述,然后把它投射到不同的坐标系中就自动产生了不同的具体表达。 可逆计算所实现的是业务的技术中立的表达,然后通过一系列的可逆转换实现它向不同坐标系的投射。 当然这种技术中立的表达本身也是需要一个坐标系的,也就是内置的 XLang 语言(包括 xlib,xpl,xdef,xdsl 等), 但是 xdsl 相当于是物理学中的内禀坐标系,一种在领域内部具有某种最优性的坐标系。 这种最优性可以通过需求变动时如何变化最少量的表达量来精确的度量,在信息论的角度上说可以进行某种精确的定义。

现代数学与古典数学的一个本质性区别在于,现代数学是建立在等价类概念基础之上的, 当们说到任何一个数学对象时,指的都不是某个特定的数学对象, 而是在某种等价规则定义下的一系列相互等价的具体对象所构成的等价类。

我们习以为常的 1,2,3 这种数学上最常见的基础对象,在严格的数学论述中也是每个数字都是由无穷多的对象所组成的等价类。

彭罗斯在《通向实在之路》一书中介绍了一个有趣的事情。他母亲的一位朋友(芭蕾舞演员)完全不能理解分数: 为什么 1/2 = 2/4,这两边明明差别很大,为什么会是一样的呢? 彭罗斯经过仔细思考发现了其中的华点:虽然他母亲的这位朋友可以说对数学一无所知, 但却通过敏锐的观察力洞察了现代数学的秘密。分数的现代定义是将 1/2, 2/4, 3/6, .... 这样的无穷序列看作是一个等价类,然后在这个等价类中选择形式最简单的 1/2 作为它的代表元, 每当们使用这个等价类的时候,们都可以用这个代表元来表示。

《通向实在之路》是一本很有趣的科普著作。如果读懂了这本书,可逆计算中涉及到的一些简单的物理数学观念就是不言自明的了。

「原子变换的复合」是什么意思?

说的是各种变换是可以复合的,并不需要一个个手工编制。 比如说可逆性是可以复合的,如果我们在字段级别建立了  文本表示 <=>  可视化展现  这种可逆变换,那么整个体系应该提供一种自然的复合方式得到一个复杂结构的   文本表示  <=>  可视化展现  这种结构,而不需要再用手工去从头开始实现。

这主要是为了由 AI 来自动找到 Delta 是么?

不是,这只是利用自然界存在的规律性而已。有人问过一个问题: 类型表达式是不是一个约束函数? 我的解释是,类型确实也是一种约束,它相当于是把运行时的一些限制投影到类型空间这样一种同态映射。 其实规律性本身就是一种约束性。但我们可以不从约束的角度去理解。 广义相对论中引力不再是一种被动的约束,而是空间弯曲后处于完全自由状态不受力的情况下自然的运动。 只是在领域空间中自由移动而己。这种移动可以确保下一个位置还在领域空间中。 对于这种情况,我们可以说费了很大的劲才确保不会破坏领域空间,最终仍然保留在了领域空间中, 也可以认为道法自然,随心所欲而不逾矩,我们很自然的就不会走到领域空间之外。 一言一行都满足领域规则,总是处于领域空间中而不自知。我们很多时候编程是为了避免偏离计划的情况出现, 好像处处都会出现问题,但是为什么不顺应规律,选择最自然的表达方式呢?

我举个例子。Nop 平台的后台是定义 BizObject,然后定义 BizObject 上的方法, 然后自动推导得到前台 rest 链接是  /r/{bizObjName}__{bizMethod}。 我们当然可以为每个对象指定不同的 rest 映射方式, 但是这样自动推导得到的标准映射方式压根就不需要编写代码,不是更好吗? 而且它保证了数学层面上的一种同态映射关系。在 GraphQL 层面它对应于 query { NopAuthUser__findPage} 这种方法,而在 grpc 层面, 它对应于 graphql.NopAuthUser/findPage,很自然的, 在多种调用协议层面,我们可以自动建立一种可逆的结构转换关系。 所以 Nop 平台中编写的服务函数,可以无需任何适配修改,就自动发布为 REST 服务、GraphQL 服务、Grpc 服务, 可以自由的在多种形式之间转换。这种转换的可行性,来源在于在信息表达层面本来就是等价的, 这种形式上的可转换性只是底层信息一致性的自然显露而已。但是有多少时候, 我们自己随意的一个链接命名就破坏了这种数学层面的自动等价性, 不得不依靠程序员手工编写适配代码来弥补其中的信息缺失。 而这种不等价性并不是业务结构内置的,只是由人的随意性所引入的。

为什么在后台我们要定义 BizObject,然后定义 bizMethod。 因为在数学层面上,我们总是要对后台的信息结构进行分解的,而如果我们进行了最粗粒度的一级分解, 给分解后的组分起一个名字,它自然的就成为对象名,然后再进行二级分解,就得到方法名。 整个 Nop 平台的设计可以从数学层面,按照最小化信息分解的规律逐步推导出来。

Nop 平台的设计是可以在数学层面上进行解释的,因为数学规律的普遍性, 所以它的思想和很多其他领域中的做法都可以进行比较。

大部分人做设计都是拍脑袋,导致对于设计的必要性是语焉不详的。 比如说比较 GraphQL 和 REST,到底孰优孰劣?有些人说各有应用场景,有些人说如果必要勿增实体, 有些人说 GraphQL 解决了 XX 问题,远远超越 REST,谁也说服不了谁。 但是从数学层面分析,GraphQL 经过形式变换后可以等价于 REST + SelectionSet, 它在数学上严格优于 REST。基于这样的认识,反向的可以认识到现有的 GraphQL 引擎实现存在问题,需要进行相应的改造。

Nop 平台并不是简单的使用数学和物理学中的概念进行类比, 就好像我们并不认为关系数据库是使用关系模型的数学理论进行类比一样。 它是严格按照数学结构进行推演并且落实到代码设计中的。 本身可逆计算的核心公式 App = Delta x-extends Generator<DSL> 是在数学层面可以进行严格定义的运算结构,其中 DSL 的 tree 结构和 x-extends 算子都具有明确的数学定义,并且可以证明 Delta 差量运算满足结合律。

一个角色对于某个字段的权限可以运行时修改吗?

可以。xmeta 中可以指定 permission,然后在线配置 role 和 permission 的关联。

这个是通过“系统表”吗?

不是。有个 NopAuthResource 表定义功能点对应的 permission,NopAuthResourceRole 表配置功能点和角色之间的关联。 在 xmeta 中可以定义字段对应的 permission。

这地方能直接对接上 casbin 之类的就好了

Nop 平台的权限设计是基于数学推理的最小化权限模型。要控制   user -- action/field 之间的许可关系,如果具有 M 个 user,N 个 action, 直接控制就是指定一个 M * N 的矩阵。如果我们要简化配置, 在数学层面就是引入矩阵分解 M * N --> M * P * Q * N, 引入两个中间矩阵,且这两个矩阵的维度都远小于 M 和 N, 则我们需要指定的配置项个数就是 M * P + P * Q + Q * N, 大大小于原始的 M * N。具体的做法就是 user -- role -- resource -- permission。 所以 casbin 之类的模型如果支持数学层面的最小化设计, 那它一定可以和 Nop 的权限模型进行适配的。

所以这个 P Q 如何选择,又是需要经验的事情?但是可以随意调整?

是否可以随意调整依赖于领域本身。在数学上确定一个结构之后,落实到领域上我们会把它对应于领域概念, 赋予某种业务含义。所以 P 对应于 role,而 Q 对应于 resource 之类的。 确定业务含义之后在领域内就一般存在着自然的粒度控制。可以这么说,我们想在结构中插入两个中间结构 PQ, 然后对比一下我们的业务知识,发现它们已经存在于业务人员的头脑中,然后可以很自然的做出选择。 实际上一个有用的结构不是因为技术层面它有用,而是因为它反映了领域内的某种规律性知识。 而一个领域如果发展一段时间相对比较成熟以后,有用的概念本身一定会有领域相关的名词对应, 并且它的粒度存在一种隐性的知识。但是,从数学层面理解之后,可以让我们更精确、更坚定的控制相关概念的应用边界, 并在多种可行的选择中选择相对成本最小的一个。

2024-02-17 要点整理

dsl 不是为了扩展点,扩展点只是一个副产品。百度 amis 的形式可以实现所有需求, 并不是百度 amis 现有的组件可以实现所有的需求。本质上 Nop 平台处理的只是形式问题, 其实所有框架如果都按照可逆计算的思想改造,那么它们可以很自然的在多种 dsl 形式之间转换。

Nop 是在更高的抽象层次上解决问题,不是使用某个特定的运行时来解决特定的需求问题。 和传统上的框架技术有着本质区别。

jsx 的灵活性导致它是无法被逆向分析的。所有可以逆向分析的 jsx 本质上都是只使用某一个 dsl 子集。

Nop 平台强调的是信息的自由流动,而不是一旦写成代码就只有程序员可以对它反向分析。

所谓的软件是为了满足客户需求,客户需求本质上是技术无关的。 而目前我们所有的写代码行为都导致客户需求与特定的技术实现绑定了,这个并不是必须的。

如何让信息可以跨系统、跨软件自由的流动?第二次工业革命是电气化,能量在多种形式之间自由转换, 都可以转换为电能。而现在信息是绑定在某种形式中,它应该需要有更灵活的流动方式。

只是要促进信息的自由流动,提供它们可以流动的结构构造规律而已。

Nop 平台所提供的不是一种低代码产品,它只是可以去做低代码产品而已。本质上是一种新的软件构造理论。 你总是从太具体的东西去理解实际上是不合适的。首先,它是一个理论。 然后这个理论带来一些新的做法,这些新的做法导致此前无解的一些技术难题可以被解决。

如果信息完备是可以在各种形式间自由转换的。但是目前的软件偏向于手工编写, 所以很多层面的信息表述不完备,从可逆计算角度的去分析就会发现这些技术存在扩展性上的问题。

每一个足够复杂的程序框架本质上都是在提供一种潜在的模型,但是很多模型没有明确的 DSL 文本表示形式,导致无法被其他工具很容易的进行分析、进行信息转换、进一步的处理等。 而 Nop 平台强调 DSL 优先,所有模型都先定义清楚自己是什么, 类似于你要写数学证明至少要先讲清楚自己所使用的概念到底是哪几个。 然后 Nop 平台会自动根据 DSL 推导得到它的可视化展现形式,自动生成可视化设计器。 而传统上你自己有一个模型,所有的 IDE 工具、可视化设计器都是要自己去写的, 还不能保证与自己的模型同步更新。

Nop 的核心内容可以看作是一个领域语言工作台,它提供理论基础、技术工具、运行时引擎等综合手段, 极大的降低开发自定义 DSL 的成本,使得每个人都可能快速开发自己的 DSL, 并自动得到 IDE 提示、可视化设计器等外围工具。我们只需要专注于元模型也就是核心信息结构的定义即可, 然后所有从数学层面可以推导得到的信息都可以自动得到。

组件技术在理论层面就无法实现系统级别的复用,必须引入差量概念才可以改变软件复用的基本原理, 从复用方向向下的整体-部分复用机制转向复用方向平级甚至向上的转化复用。  从相同才可以复用扩展到相关或者相似就可以复用。这是从基本的软件复用原理方面的变革。

可逆计算的核心结构空间一定是 DSL,也就是自定义的结构空间,而不是某种通用的程序语言结构空间。 试图提供一种通用程序语言,它只会是可逆计算的一个非常局限的应用,而不是可逆计算理论所指向的核心内容。

  • 一种语言就是一个坐标系统,用语言去表达业务相当于是用坐标系的描述去表达几何对象
  • 微分流形理论(大概是 18 世纪高斯和黎曼时代的知识)认识到无法将局部的欧式坐标系推广到几何体整体, 必须使用多个局部坐标系,然后把它们拼接在一起。Nop 平台提供了创建新的 DSL 和粘结 DSL 的工具, 相当于是使用多个局部坐标系来描述业务
  • 在坐标系统上引入更加灵活的差量运算,使得结构构造规律更加丰富化

从可逆计算理论可以非常明显的看出,现在很多做低代码的厂商试图建立一个通用的 DSL 来覆盖所有的业务,这在本质上是徒劳的。因为复杂对象的降维描述本质上需要的是多个坐标系统。

  • 我们需要使用领域特定的坐标系,所以需要多个 DSL
  • 在结构层面,所有的 DSL 都可以采用 Tree 结构来表达
  • Nop 平台在结构层面相当于是提供了一组通用的 Tree 结构的生成、解析、转换工具。 而传统的程序语言在结构层面表现为类-属性这种短程结构,相当于是 Map

应该说绝大多数人没有想到 delta 可以表达减法和逆变换的思想。在学术界就很少有这种想法, 以至于提出 Delta Oriented Programming 的这个人在德国靠 DOP 评上了教授。 另外,没有人想到 Delta 可以是非常通用的概念,大量的做法都是针对某种结构定义 delta, 没有认识到 Delta 这个概念的抽象性和普遍性。

认识到 Delta 的少数人也没有认识到 Y = F(X) + Delta 才是完整的表述, 可逆性实际上在这个公式中实际上体现在两个方面,一是 Delta 差量合并,二是 F 可以是某种可逆变换。 在 Nop 平台中是系统化的应用数学上的同构和同态变换的思想, 尽量通过原子变换的复合来构造更复杂的结构变换。

信息空间中最有效的生产方式不是组装,而是掌握和制定运算规则

为了克服组件理论的局限,我们需要重新认识软件的抽象本质。 软件是在抽象的逻辑世界中存在的一种信息产品,信息并不是物质。 抽象世界的构造和生产规律与物质世界是有着本质不同的。 物质产品的生产总是有成本的,而复制软件的边际成本却可以是 0。 将桌子从房间中移走在物质世界中必须要经过门或窗, 但在抽象的信息空间中却只需要将桌子的坐标从 x 改为 -x 而已。 抽象元素之间的运算关系并不受众多物理约束的限制, 因此信息空间中最有效的生产方式不是组装,而是掌握和制定运算规则

Nop 与其他工具之间最根本的差异是 —— Nop 背后的原理是「客观规律」, 是根据已有数学与科学研究「推导」出来;而其他工具是根据开发活动经验的累积「总结」出来的。 这两者之间在逻辑层面的严密性差距不言而喻。

注解或者超链接这种形式其实老早就有了,都知道 word 其实是 xml

你没有能理解这里真正重要的数学层面的构造,只关注到一些表观的细节, 以为使用了注解或者超链接这种形式就是设计的全部。 你应该仔细读一下文章,努力去尝试理解与自己思维不同的部分, 而不是在文章中只看到自己曾经熟悉的名词片段,这样的话你永远理解不了新的设计。

在 NopReport 的设计中,关键的部分是 Template = 原始模型 + 扩展配置。 也就是说,任意的原始模型都是一个合法的模板,我们是在原始信息之外再补充扩展配置即可。

Template = BaseModel + ExtModel
TemplateEditor = BaseModelEditor + ExtModeEditor

更进一步的推理是,我们可以在保持原信息的可视化设计的同时, 补充一个扩展模型的可视化配置,就可以实现报表模板的配置。 利用这种方案,我们可以实现任何模型的模板化扩展,而且只需要补充 Delta 信息。

如果 Office 内置了某种扩展机制,比如说 Office2003 的自定义标签机制, 那么可以直接使用,根本不需要用到超链接。 正是因为 Office 的设计没有按照可逆计算理论进行,才使得我们无法直接将它作为模板编辑器, 需要做一些额外的形式转换工作。

你只看到了 Office 使用了 XML,XML 比 POI 好处理,但是没有理解为什么 XML 比 POI 好处理。 XML 是通用的结构层面的表达,而 POI 对象是有着不同的数据类型的对象层面的表达。 可逆计算强调我们在将信息转化为对象之前,存在统一的结构表达层可以直接在这个层面完成很多通用操作没有必要把处理放到对象层。 对象层每个对象的类型不同,处理规则是不同的。类似于不同的建筑具有同样的工程力学, 在结构层看来,很多业务层面不同的东西本质上是一样的。 不使用 XML 表达,使用 JSON 或者任何一种通用结构化形式来表达也是一样的。 而且 Nop 平台的设计不需要做任何改变,只需要插入一个形式转换适配器即可。

你也没有理解 XPL 模板语言在其中的作用。为什么有了通用的结构层表达之后,立刻就可以得到它的可执行态。 XPL 在数学上的定位是什么?它将任意的 AST 表达树通过语法制导翻译转换为可执行逻辑。

这是一种系统化的解决方案,并不是仅仅用于 Excel 和 Word 生成的一种聪明的技巧。 你只有理解它在数学上对应于什么样的通用变换,才能理解它为什么这么简单却有用。

另外,选择超链接是因为目前在 Word 中它的可视化形式比较直观,而且插入后位置定位比较准确。 其他方式,比如插入域等在结构层往往会错位。

网易 codewave 的设计怎么样?

codewave 的 DSL 设计不行

主要是在哪,我看他们是实现了一套 NASL 的东西

从可逆计算的角度分析,NASL 处处都是问题:

  • 没有统一的元模型
  • 应该是多个 DSL,DSL 应该是允许自定义扩展的,而不是固化死板的一个
  • 没有差量定义
  • 任何一个具有足够内在丰富性的语言都应该提供分解、合并、二次抽象的手段,NASL 没有
  • 缺少自动推理得到可视化界面、IDE 设计工具这些外围支持

拍脑袋设计语言,靠经验积累属性。

DSL 本身是可编程的,即便有一个固有的规则应该可扩展性也很高吧,像是苹果刚开源的 pkl。

不是。可编程不意味着可扩展。你比较一下 nop 平台中的 dsl 和 xpl 模板语言就知道了。

基于可逆计算,有领域坐标系,可以进行可逆变换的复合,就自动支持可视化了。

不需要特殊语言设计,本质上只是信息结构的设计

形式不重要。在数学中一切形式都是虚妄

我理解是不是这个样子。像是普通的 DSL 描述主要是做到:

  • 元数据的描述
  • 数据本身的表达
    • 以上两者是一对一,不同定义之间是隔离的,部分 DSL 达到了
  • 元数据以及数据本身表达的模式是可编程的,动态的

NOP 在顶层又抽象了一层做到了:

  • 数据本身的表达是可以互相抽象组合的,即便数据的结构基于不同的元数据定义
  • 元数据定义也是作为一种数据存在,本身也有自己的元数据

所以 NOP 里的 DSL 分解组装、组装,本质就是数据的结构变换,DSL 的定义可以最小化表达。

大部分 DSL 没有明确的元模型。定义自己的 DSL 的时候应该加一个 schema 属性,指向自己的元模型。 DSL 的本质性价值是对领域内逻辑的高密度表达,这个具体抽象的是否合适要看是否满足领域需求, Nop 平台的 DSL 也只是一些示例。通过对比,可以发现其他类似的 DSL 设计存在哪些问题。

可逆计算指出的是结构构造具有统一的规律,所有 DSL 可以共享这些规律,这里就是一些纯形式层面的做法, 与业务领域无关了。但是这些规律使得我们开发 DSL 的时候可以更加灵活、简单, 这里涉及到 DSL 的无缝嵌入,动态转换、自动推理等,这些能力在一般的 DSL 设计里是没有的。

一般的 DSL 设计就相当于一个工匠做了一把锤子来解决某几个钉子的问题,但是要搞懂牛顿力学, 你才能普遍性的解决所有力学相关的问题。

如果把所有 DSL 所构成的空间看作是一个物理世界,我们要问这个世界中的牛顿力学是什么?

nop 的扩展性很灵活颗粒度可以很小,那么当在这个之上做扩展,怎么避免不恰当配置导致功能受影响?

比如把某个源的 bean 关了就影响了功能

Nop 只解决形式空间的变换问题。最终语义空间的问题需要自己想办法保证,自己增加约束校验。 框架层面在各类引擎会进行一部分的语义检查:

  • xdef 元模型中会定义格式要求,模型合并完之后会经过 validator 校验
  • IoC 容器在 mvn install 打包以及系统启动的时候会自动检查 bean 的依赖关系
  • ORM 启动的时候会检查所有的表引用和属性引用正确
  • XView 模型会检查所有引用的字段如果没有标记为 custom,则必须在 XMeta 中定义

总之,在各类引擎中,可以根据自己的语义增加一些检查,类似于增强型的类型检查。 xdef 就是比程序语言类型更强的一种检查。但是各个引擎内部可以引入业务语义相关的更强的事前约束。

可逆计算理论中,Delta 合并发生在结构层,此时各种运行时非法的结构都在可以存在的, 因此这里是可行空间,是所有可能出现的结构的最大集合。类似于量子力学在观测之外是可以打破能量守恒定律的, 各种量子隧穿是允许的。但是合并完成之后,真正进入可观测、可运行的世界,这时需要经过一系列的规律约束。 Nop 平台内置了多阶段编译能力,在真正运行前提供了很多图灵完备、应用层可介入的验证时机, 甚至可以类似于 contract based programming,插入单元测试,在调试模式下通过某些样例测试才能启动。

传统的编程中,结构空间是封闭的,它仅由编译器厂商定义。Nop 平台相当于开放了结构空间, 允许自定义的结构规则,将编译器的能力开放给应用层。前端领域,随着 babel 的流行, 一定程度上已经开放了编译器的部分能力,但是除了少数框架,真正在应用层能用上 babel 的能力的很少。 Nop 提出一系列的结构规则和使用模式,使得这种能力的使用变得更加简单、直观。

概念层面很简单,就是如果把软件的结构空间看作是一个世界,那么就要问, 这个世界的牛顿力学和微积分是什么?它的基本的结构构造规律是什么?

结构空间是不是就是指 AST,AST 的特性就是牛顿力学,微积分就是语法制导?

或者可以理解为,AST 的每一个节点上通过标签加算子,这样的一个算子就是 delta?

语义就是这个世界的运行规则?

在可逆计算理论中,结构空间就是一切用 Tree 结构表达可及的空间,其中包含 AST, 还有传统上我们不认为是 AST,但是概念层面等价于 AST 的 Tree 结构,比如文件系统树。 在这个结构空间中的微积分,第一步肯定是需要建立差量的概念,然后再定义差量如何合并, 如何根据差量产生差量等。语义实际上是对结构的解释。数学层面上一切都是形式, 各种不同的东西都可以对应于同一个形式。但是在特定的应用中,我们会把同样的数学结构解释为不同的物理意义。

在现代物理学中,牛顿力学实际上可以看作是一种最简单的因果关联手段。 假设结果和原因之间存在线性关系,力学中的结果就是加速度,然后我们把它的原因定义为力, 实际上不是先有力,再有加速度,而是我们观察到加速度,然后反向查找,假定存在一个原因叫做力。 结果 = 系数 * 原因。所以在广义相对论中,最终力的概念被几何空间的自然趋势所代替, 之所以这样运动,不是因为受到了力,而是因为空间本来就是这样弯曲的。

如果拿序列图来讲,一个序列图代表一个业务流程,通过表达形成 Tree, 这个 Tree 是执行 Tree,操作的是业务数据,最终达到业务目标。

差量最初级是 java 中的一个语句,语句的合并形成一个固化的业务操作, 差量要想用好本质是要从数学的意义上去分解具有正交变化的业务操作最小单位, 然后通过 Tree 组合在一起,形成特定的业务。

如果找不到也没有关系,通过量的积累从中去找到最小的变化。

如果相对于现有的软件工程而言,差量最重要的概念是它是独立存在的,可以独立于本体存在, 并且差量可以复合产生更大结构的差量,最终全量也是差量的一个特例。 具有一定结构的差量一定是正和负的混合体。从局部变化的角度去考虑系统的构造, 相当于把系统放到时间轴上观察它的演化,然后从局部的变化如何累计成为更大粒度的变化, 但是这种变化仍然可以从本体上被剥离下来独立的研究。

观察的重点不是这个本体是什么,而是它的变化是什么。其实世界的本体是不可观测的, 我们观测的所有知识都是关于变化的知识。本体再复杂,它也只会成为一个背景, 并不影响到我们对于当前业务的处理。类似于操作系统再复杂,我们关心的业务也只是用到少数操作系统的 API 接口, 我们关注的重点是我们的业务如何实现,虽然它必然要运行在一个很复杂的操作系统之上, 但这只是背景信息。我们是为业务应用买单,而不是为它运行在其上的操作系统买单。 Docker 提供了一种切片机制,可以将应用切片在某些意义上从操作系统上剥离下来,然后独立的存放、管理、分发。

我们理解的单元是应用系统本身,而不是 应用系统 + 操作系统 这个整体。

经济学中的“边际革命”指的是 19 世纪末至 20 世纪初的一场理论变革,它主要改变了经济学分析的方法和重点。 这场革命的核心在于对“边际效用”(marginal utility)和“边际生产力”(marginal productivity)的概念进行了深入研究, 并强调了它们在解释经济行为和市场均衡中的重要性。

边际效用是指消费者在消费过程中,每增加消费一单位商品所增加的额外满足或效用。

经济学家如卡尔·门格尔(Carl Menger)、威廉·斯坦利·杰文斯(William Stanley Jevons)和利昂·瓦尔拉斯(Leon Walras)等人认为, 边际效用是决定商品价格的关键因素。他们主张,商品的价格是由消费者对最后一单位商品的愿付价格决定的,即边际效用决定的。

另一方面,边际生产力革命则是由阿尔弗雷德·马歇尔(Alfred Marshall)等人发展起来的, 他们认为生产要素的价格是由它们的边际生产力决定的。也就是说,一个要素的工资或租金取决于它对生产额外单位产品的能力。 边际革命对经济学的影响深远,它使得经济学从关注总量转移到了关注边际量,从而更加精确地描述了市场经济中的个体行为和决策过程。 这场革命也为后来的微观经济学理论发展奠定了坚实的基础,并影响了后来的经济分析方法。

差量化对我们最有用的应该是:

  • 代码的复用,可以支持最小化单元
  • 方便拓展,支持个性化
  • 在线设计可以丝滑转人工,不需要额外的工作

这是最初级的理解,可逆计算理论所指向的远不止这些。

为啥改代码的地方可以跟坐标对应起来?

可逆计算相关的技术文章对阅读者的要求有点高。 单单一个修改点的坐标化,很多人都无法理解,为啥改代码的地方可以跟坐标对应起来。

大部分人压根没有接触过三维以上的坐标系,更不知道高维空间和非欧子流形的概念, 所以坐标系压根就是一个无内容的概念。

在说到协程时,我曾指出结构化编程自然引入了一个坐标系统,而协程的实现本质上是记录执行空间的坐标, 然后暂停,最后再跳转到指定坐标处。就有人就反对说不需要坐标的概念,协程只需要记录上下文就可以。

但是这里的上下文是一个笼统的概念,整个内存都是上下文,但是坐标系是一个系统化的概念, 只需要少量坐标就足以描述整个运行时空间,而且它保证是唯一的、无遗漏的, 但是很多人在概念层面是无法分辨这些概念之间的区别的。

涉及到空间,就得有坐标系。

坐标系就是用来在某个空间里进行定位的,哪行哪个字符也是一种坐标系。

Nop 的数据输入验证方式

可逆计算有实现 jr303 吗?

没有。这个太简单了,没什么用。xmeta 中的验证规则要更多,更全。 如果不是实体对象,是自己写的 Bean 对象,目前还缺少一些自动验证手段, 计划是根据 bean 自动生成一个 meta 对象用于验证,但目前还没有做。

现在可以通过 @PropMeta 注解来增加类似 jsr303 的验证规则。

Nop 平台的核心模型是非常完善的,因为它的设计具体包含什么内容不是根据用户需求来确定的,而是由数学的完备性来确定的。 但是它在具体的应用层使用的时候需要一些 binding,这些 binding 只有用到的时候才会根据具体的需求来进行适配。 比如说所谓的验证本质上是在对象结构上附加一些额外的约束规则,而 Schema 模型的定位就是来定义结构以及结构上的附加约束。 Nop 平台中所有的结构按照数学定义都具有对应的 Schema 模型,比如 xdef 元模型和 objMeta 元数据模型之间可以互相转换。 具体如何构造 schema 是一个额外的形式转换问题。JavaObjMetaParser 可以解析 XJava 文件得到 ObjMeta 元数据。 @PropMeta 提供了通过反射注解提供元数据的一种方式。现有的技术框架一般没有信息中立表达的概念,费半天劲定义的信息仅能在自己内部使用, 无法和外部共享信息结构模型,导致重复表达。

如果稍微复杂一点的输入验证怎么做比较好?比如根据其他表的配置以及实体当前状态做一些条件判断之类, 我现在通过覆盖 crudModelupdate,判断之后再调用 doUpdate,不懂这样做是否符合 Nop 的思路。

这个参见 validator.md, 可以定义一个独立的验证模型。在 xbiz 文件里使用的时候就非常直观。

doUpdate 有个回调 beforeUpdate,如果手工编程可以在这个回调函数中执行额外的逻辑。

这个文档看过,但是没看到它支持复杂的编程,像刚才说的另外查表再做判断。

在 xbiz 中可以

<action id="update">
<source>
<bo:DoUpdate data="${data}">
<prepareUpdate>
<biz:Validator
fatalSeverity="100"
obj="${entityData}">

<check id="checkTransferCode"
errorCode="test.not-transfer-code"
errorDescription="扫入的码不是流转码"
>
<eq name="entity.flowMode" value="1"/>
</check>
</biz:Validator>
</prepareUpdate>
</bo:DoUpdate>
</source>
</action>

首先,所有的外键关联字段其实已经经过了校验。

如果要获取其他表的数据来进行验证,首先看是否是常用的某种模式,如果是模式,则可以通过标签进行抽象。 如果不是,则可以直接使用 <bo:FindFirst bizObjName="sss" xpt:ret="refData"> 这种标签获取数据到变量中,后面就可以在 validator 中调用。

整体思路是通过 xbiz 结合 validator 来实现对吗?细节我再去学习研究下,xbiz 还没用过。

xbiz 整合 validator 可以更加直观,减少额外的信息噪声。如果是一般性的业务代码,直接在 java 里通过 prepareSave 回调里写普通的函数验证就可以了。获取关联数据使用 dao 上的方法一般很简单。 需要注意的是,一般情况下我们应该在 Java 中把领域模型对象的结构梳理清楚,通过对象的属性访问就可以直接获取到关联对象每次都去查询数据是不正确的做法。应该是有一个领域模型对象,直接通过点点点就可以直接获取到对象。比如下面的领域模型接口:

interface IAccountBo{
ICustomBo getCustomerBo();
IAccountBo getPrimaryAccountBo();
...
}

嗯,ORM 的正常使用应该是点点点这样的,我那个场景特殊点,是通过一些全局的不直接相关的配置表来查信息。

那一般情况下也应该有一个聚合根对象来管理所有相关的配置信息。DDD 的做法应该是先获取到聚合根, 然后,后面的编程就完全只在对象层面进行。这种情况下很容易就使用 Validator 这种验证模型。 另外 validator 中可以直接调用标签库:

<biz:Validator checkLibPath="/xxx/biz!check.xlib">
<check>
<biz:IsXXX a="1" />
</check>
</biz:Validator>

只有逐步开始使用元编程,并且实现产品化产品(同一个产品特性存在多种定制化差量逻辑), 在长达数年的生命周期内并行演化多个分支的时候,才能真正体会到 Nop 平台独一无二的能力。

Nop 平台很容易解决全局规则类的需求:所有 xxx 都要 yyy,而现有的软件工程一般很难提供一种横跨多个抽象层面的统一业务规则抽象。

为什么 DDD 需要使用所谓的六边形架构,本质上是要摆脱输入输出的结构和形式约束,领域内的结构逻辑应该是不依赖于输入输出的纯逻辑。 为什么感觉需要获取别的表的数据来参与校验会比较复杂,本质上这是因为我们访问数据的时候需要对象信息 + 访问外部存储需要的环境信息, 而获取环境信息需要额外表达,至少需要指定对象名,获取到 dao,还需要准备条件,执行查询操作等。但是如果已经建立了领域模型抽象, 在纯逻辑层面, entity.relatedEntity 直接就可以访问到对应的信息,没有任何环境依赖,不需要任何输入输出相关的知识, 所有的符号都是领域内的,且是最小化表达的。领域模型的价值相当于是建立一种访问领域信息的最短路径, 先直接达到聚合根(相当于是某种枢纽节点),然后从聚合根到所有领域结构节点都有最优访问路径。

这个意思大概能理解。不理解的是,比如配置相关信息从一个配置的根出发能访问到,但是获取这个根的本身, 不是也要执行前面说的查询过程吗?因为从当前实体无法访问没有直接关系的配置聚合根。

是要查询,但是这个查询是只需要定位到聚合根就可以,并不是要定位到你这次请求需要使用的数据。 聚合根是很粗的对象。比如 Product --> Product Params + ProductCategory + ..., 我们只需要定位到 Product,聚合根内部是靠领域逻辑来获取。 这就相当于你要到某个地方去,是先坐飞机或者高铁到核心站点,然后再坐汽车,最后步行,不是直接坐飞机到每个地方的。

嗯嗯,这个能理解,目前也是朝着这个方向做,其实从业务出发设计实体,挺自然就会按这个思路,即使不懂聚合根这种概念。

不过有时从不同角度考虑,这个根并不总是很明显,一种场景下这个是根,另一种场景下似乎换一个根更合理。

一个领域本来就是可以有多个聚合,所以会有多个聚合根很正常。

聚合根在业务上具有某种内聚性,但是在对象图上往往是相互引用的,并不存在单一根节点的情况。比如:

interface IAccountBo{
ICustomBo getCustomerBo();
IAccountBo getPrimaryAccountBo();
}

interface ICustomerBo{
List<IAccountBo> getAllAccounts();
}

这里 custumerBoaccountBo 都是聚合根,从不同的途径可以达到,然后它们之间还可以直接引用。

聚合根的概念与前端因技术限制导致的单一状态树并没有关系,也并不一致。

有时,明显的根,比如父子表,或者再子表,这个父很明显是个根。 但是有些操作,一把根据某些条件之类查出子表或者子子表的数据,处理完毕,不需要关心父表, 简单粗暴,但很实用,这时再去考虑根不根的就觉得很多余。

其实关系数据库是一个很强大的对称访问模型,任何表和字段在某种程度上都是平权的,我们可以用同样的方式访问到所有数据。 这在原则上是一种进步,并不是需要被反对的情况。但是在高层认知层面,复杂结构一般会出现对称破缺,不同的访问路径并不是平权的, 我们会发现某些路径有特殊性。我们可以利用这种特殊性,从而简化认知。 关系模型 + 聚合模型,我们可以同时使用这两种视角, 同时还可以维持数据的一致性,这就是底层关系模型所带来的强大能力。

如何理解表象变换和可逆转换?

实际应用中我们接触的到变换大部分是近似等价

A ~ F(B),
G(A) ~ B

通过补充差量可以使得上面的关系成为等式

A + dA = F(B + dB),
G(A + dA) = B + dB

因此,在 Nop 平台中一个关键设计就是所有细节处都允许存放扩展信息,也就是说永远采用 (data, ext_data) 这种配对的设计。当信息在某种形式中溢出的时候,可以找到地方存放。

在物理学中,所谓的可逆就意味着熵保持不变,也就是信息的守恒。 需要像理解能量守恒定律那样理解这里的内在的规律性,这需要学过一点物理学。 否则就会钻入细节中,总是在说凭什么永动机是不可能的?

备注

要保持系统整体的熵不变,则需要确保在转换过程中,所有信息均不丢失, 因此,需要以 (data, ext_data) 形式让额外的信息得以保留。

表象变换还需要和最小化信息表达结合起来去理解。 最小化信息表达意味着达到某种最优性,而达到最优性的东西一般是唯一的, 否则,我们就可以比较两个不同的东西,继续选择其中更优的那一个。 而在数学上所谓的唯一性其实是通过等价关系来定义的。 也就是说,数学上说 A=B 的时候并不是说 A 就是和 B 一摸一样的东西, 而是说 A 和 B 之间存在双向可逆转换。 因此,如果我们在设计中达到了某种最小化信息表达,那么这种信息结构投射到不同的表象中, 这些表象就应该是可以进行可逆转换的。

DSL 和通用语言之间的转换往往是非线性的,因此它有可能实现不一样的解耦手段。 在现代信号系统理论中,表象变换一个核心概念是所谓的稀疏性, 也就是映射到某个表象中之后只有少数几个维度有值,相当于实现了降维

备注

降维后溢出的信息(未被映射的部分),便存放在 (data, ext_data) 中的 ext_data

Nop 与 Git 差量的区别?

  • git 在管理以行为单位的程序代码时暴露了很多问题,文章认为这是因为行的差量(变更)不满足结合律。 而 docker 以文件为单位的差量管理是满足结合律的,但是在处理文本时又存在粒度不够细的问题。 nop 似乎是以 json/xml 的元素作为处理单位的,那么 nop 是怎么处理的呢?
  • 除了 json/xml 以外还能处理什么别的文档,如果是图片,二进制文件之类的 是回落到 docker 的基于文件的方式吗,还是有什么别的办法?
  • monad 使得函数作为一等公民可以进行某些运算?但是文中提到 monad 是没有逆元的,那么 nop 在这之上,包括引入伴随函子,做了什么扩展?
  • 题外话,除了单位元以外还有类似乘以 0 这种零元,这玩意有意义吗?

JSON Patch 的格式与原始 JSON 的格式是完全不同的,而且只能使用下标来标记列表中的行,而 Nop 中的 Delta 合并是通过 name 等唯一属性来定义行,在语义层面更加稳定(插入一行不会影响到所有后续行的定位), 而且差量的格式与原始格式完全一致,只是额外引入了一个 x:override 属性

那么我也可以说 json 的 delta 可以和原始格式完全一致,只要额外引入一个 __override 属性。

Git 的 diff 功能是把文本文件先拆分成行,然后再对文本行的列表进行比较。 因此,Git 的差量结构空间可以看作是行文本空间。这是一个通用的差量结构空间。

这里读起来不太对,因为 git 对于二进制一样可以生成 delta,对于二进制就没有什么行文本空间。 这里的行文本空间概念比较别扭。

感觉对于二进制,git 和 docker 的处理方式是否相通?这里说的行空间可能是针对文本的 但很容易引出另一个问题:如果我的代码全都 pack 到一行 然后改一个字符,git 怎么办?

nop 又会怎么办?

我认为 git 内部应该不是按行来处理的,看起来是按行处理那是为了生成人可读的 diff 文本 人要读才需要换行,我想 git 内部对行没有特殊处理。

Nop 是通过 XDef 元模型引入领域坐标系, XML 的每个节点和属性都有唯一的坐标,也就是 xpath, 从而实现类似 Docker 在文件结构空间中的合并运算。当然 XDef 中的合并算法不是简单的 replace,要更加丰富。

二进制文件没有合适的内部坐标系可用的情况下,目前就是整体替换。所有的 Delta 要发生作用,首先需要一个坐标系统。 有什么样的领域结构空间,就有什么样的 Delta。

Monad 本质上就是函数的结合律,只是它说的不是普通的函数,而是 a -> m b 这种具有额外包装形式的函数, 从而把副作用、异步处理等逻辑也纳入到函数的结合律可描述的范畴。 具体参见文章写给小白的Monad指北。 伴随函子只是在强调范畴论中也是非常需要某种可逆性的。不考虑可逆性的结构很难有真正的大用。

零元的作用可以看作是造成空间的坍塌,任何元素与 0 相乘都变成了 0。可以利用这一点用于简化系统结构。 比如说内置一个 feature 开关子系统,feature 关闭的时候整个一片功能都归零。

那我现在的理解是这样的,git 这样的 delta 是专注于通用数据而不管数据的内部结构的。 NOP 要的 delta 需要知道数据的内部结构,它的 delta 是对于结构化数据内部某部分数据和操作合并一起的。

拿网络协议做类比,git 工作在网络传输层例如 tcp,tcp 是不去管数据的内部结构的, 而 NOP 工作在应用层,比如浏览器,需要解析数据的内部结构。

我认为关键的差别应该是这样的,工作在不同的层次,docker 也是需要知道操作数据的结构的,也是工作在应用层。

这个理解是错误的。我所描述的所有内容都是概念层面上的东西,而不是具体的某种实现技术。 实际上你可以考虑一下,使用文本格式的 diff 进行交换,git 所有的接口和功能都是可以保留的, 而且目前 git 提供了生成 diff 和 apply diff 的功能,这是它的标准接口。 我们只从接口层面认知一个事物,这个 diff 就是 git 所提供的标准差量格式。 当然二进制的问题是一个额外处理的问题,最基本的做法就是完整拷贝就可以了。 一些简化的 git 实现就是这么做的。git 用于大文件的情况逐步多了之后,有些人有开始优化底层的存储,这是另外一个问题。 我文章中已经说了,同样一个物理变化其实存在多种差量形式,差量形式并不是唯一的。 即使 git 内部对于二进制文件差量保存了其他格式,它也是和 diff 文本格式是可以相互转化的。 这就是一种表象变换,在数学层面上称为是等价关系。

那 nop 到底是一种差量格式还是?

还是强调从外部接口层面看它是怎么办,当这个差量打包出来之后,它是否满足结合律等等, 是否具有独立的工具可以去构造这些差量,这些是抽象的运算关系。 一个完全内在的格式,没有其他工具可以很容易的去构造的差量形式本质上没有多大用处。

但是如果每次讨论的时候都要结合具体的外部接口,有可能会导致混淆基本概念和接口的具体实现。

可逆计算是关于差量概念的一个通用理论。Docker 就是可逆计算的一个具体应用实例。 Nop 是可逆计算理论的一个参考实现,这个参考实现中提供了我们构造领域特定的差量空间的通用工具和手段。 你要复用已有的差量空间也是可以的。但是领域特定的差量空间它的运算更加稳定,受到小的扰动后可以只出现局部的变化。

我感觉这里其实只说了 docker 所依赖的 OverlayFS,它只对接标准文件接口。没有谈 docker 的其他能力, 你在尝试把那些内容拉进来讨论的做法本身是错误的。

Docker 是利用了已有的文件系统。所有 Linux 上的工作本来就已经沿着文件系统这个命名空间管理系统进行了对齐。 文件内部的结构 Docker 是不触及的。但是很多地方没有一个既定的领域结构空间的情况下,从数学上的推理出发, 我们可以构造一个出来,从而实现类似 docker 的效果。 现在人们已经逐步接受了不可变数据的概念,而与不可变数据对偶的不可变逻辑的概念要怎么体现出来呢。

就是上面的问题啊,如果混了太多其他能力,就不好区分哪些是 nop 自己的了。

不可变逻辑,函数,或者程序本身?

Nop 是围绕着差量概念构造的一整套架构和工具,差量的格式是其中的一部分,还有可逆转换等一系列内容。

我去看了 git 源码里的 diff-delta.c,里面没看到对文本和二进制分开处理,所以看起来不是根据行来处理的。 这部分我只是看到你的说法里一个小问题,和你要表达的本质没太大冲突。 但是从我现在的理解来说,最本质的差别还是 delta 是否有针对具体的数据内部结构来制作。

git 本身它是不满足结合律的,所以对文本进行规范化后总是可以和 base 相比,成为二进制块进行处理。 我说的是概念层面上的内容,你完全可以抛弃 git 现有的实现自己去写一个满足 git 的接口的东西, 它仍然具有 git 所提供的所有价值。

照这个例子是不是可以用把 diff 换成 nop 来显示 nop 支持的某些格式的差量?

但是从我现在的理解来说,最本质的差别还是 delta 是否有针对具体的数据内部结构来制作。 因为要考虑到 Delta 的独立性和它的可组合性,最终它必须具有独立的结构定义,否则对外部来说就是黑箱的, 没有参与处理的余地。所以一个独立的、可用第三方工具和手段进行操纵的 Delta 一定是有某种结构的。

除了 json/xml 以外 可能还有些别的文档类数据,比如 pdf 甚至 excel,那又如何处理呢?

能够深入到数据内部结构中做部分数据的增删改查的数据格式,就可以用类似的方法吧。

如果按群主的思路,这个应该叫形式转换,即 xml、json、excel 等表现方式层面的东西都互相转换,是等价的。 重要的是把形式映射到他这套体系中,这个体系的实现就是坐标。

也就是说能改就能 nop 改不了就二进制 diff?

这里永远都是一个问题,根据可逆计算理论的分析,你需要定义 Delta,必须要考虑 Delta 运算的封闭性、结合律,单位元和逆元。 Excel 的问题其实很简单,我们提供了一个 workbook.xdef,可以把我们所用到 Excel 模板转换为 XML 格式表达, 从而就可以利用 XLang 内置的 DeltaMergerDeltaDiffer 工具来处理。

你想一下,如果你能够这么做,如果你能够把分散在各处的微小变化收集到一起,汇聚为一个更大的 Delta, 那么意味着在原系统中一定存在一个定位坐标系的,否则你分离后的 Delta 怎么知道它要应用到哪里呢?

"定位坐标系"用系统的词汇来表达大概就是"路径"吧,或者"路径树"?

二进制本质上也有定位坐标,就是二进制的字节位置,但是这个坐标没有领域意义, 在表达领域问题的时候是不稳定的,因此我们无法有效的利用其他工具去发挥它的最大价值。

所谓的坐标系,本质上就是提供 get(position)set(position, value) 这样一个配对的函数。 position 要能够唯一定位到一个坐标点而已。路径树是一种很自然的坐标选择,而且我在文中强调了 tree path 实际上是绝对坐标和相对坐标的统一,在父节点范围内,我们可以通过 namesubpath 进行相对定位。

嗯,有一些结构化二进制数据,比如 protobuf 他是有比较完善的形式化描述的。

二进制的字节位置,和数据内部结构没有直接关系,所以最终根据二进制做出来的 diff 就没法和数据内部结构产生关联,这是比较本质的。

是的,是不是二进制其实不关键,关键是在概念层面上是否存在坐标系统

可逆计算理论其实只是提出差量结构在理论层面的重要性,应该从更抽象的层面去考察程序结构的构造, 然后在这个思想的指导下定义了一个标准化的技术路线 App = Delta x-extends Generator<DSL>。 最早的软件其实是数学,最早的编程者是数学家,但是随着软件的广泛应用,我们逐渐引入传统制造业的一些常识, 比如框架、蓝图、组件这些概念都是从建筑业中来的,设计模式也来自建筑学理论家的著作。 但是软件不是物质世界的产物,它是逻辑空间中的抽象结构,它的构造规律并不需要受到物质世界构造规律的制约, 而在抽象的逻辑空间中,最高效的生产手段是引入规则,通过规则自动推导产生。那么一个重要的问题就是,最基本的可以构造的规则是什么?

可以先想清楚 Docker 是怎么做的,然后 K8s 中的 Kustomize 技术是怎么做的。 Nop 中的 Delta 是更加系统化的一种做法,它是从理论出发,而不是基于实践经验总结得到的。可以跟踪源码看一下。

所有这些基于差量的实践背后存在统一的理论,Nop 是这个理论的一个参考实现。本质上 Nop 不是一个特殊的设计技巧。

真正重要的是这个理论的框架以及在这个框架的指导下很自然的做出的架构选择。在这种架构选择下很自然的具有的功能。 比如这篇文章从张量积看低代码平台的设计

如果把 git 的 diff 换成 nop 的 diff,可以创造一种领域特定的差量管理工具。Nop 内置的 VirtualFileSystem 本质上在做这个事情。