.. Kenneth Lee 版权所有 2020-2021 :Authors: Kenneth Lee :Version: 1.0 逻辑闭包和抽象概念定义 ********************** 逻辑闭包 ======== 我尝试在本文中定义一个在架构设计讨论中经常要用到的概念,我把它称为逻辑闭包, Closure。取它在数学意义上的意象。 数学上的闭包的定义是这样的: | In mathematics, closure describes the case when the results | of a mathematical operation are always defined. 简单说,我在一个集合中定义了一组成员和针对这组成员的操作,这些操作的结果,仍在 这个集合之内。 数学上这是非常严格的定义,但在构架设计上我们做不到这么严格,因为架构设计是个逐 步发现边界的过程,什么东西放在A模块中,什么东西放在B模块中,或者什么东西放在层 一,什么东西放在层二,这是逐步细化和维护中“决定”的。注意,这不是发现的,而是决 定的。这个边界并非天然存在,是我们做了选择,后面在不断发现新的信息和引入的新需 求在强化这个边界。所以,这里引入逻辑闭包这个概念,是要给出一个逻辑空间,这个空 间引入的概念,进行逻辑推演后,结论的描述仍在这个逻辑空间的概念范围内,而不需要 引入额外的信息去补充。 特别要提醒的是,逻辑空间中的概念,只针对抽象概念本身,不包含被抽象的对象细节。 比如我们说我们“每个模块都有模块名”,这个讨论上下文定义的逻辑空间就只包含模块和 它的属性:模块名。但我们不包含这个模块的入口地址,也许这个模块确实存在入口地址 ,但我们的空间中没有抽象“入口地址”这个概念,“入口地址”就不属于这个逻辑空间,也 不属于这个逻辑闭包。 比如一个链路层的逻辑空间,它包含节点,链路,报文,Payload这样一些概念。它可以 构成一个逻辑闭包,因为我们讨论链路有关的东西,只需要用到这些概念就够了,我们决 定是否重发一个报文,只需要知道报文的目标节点是否回了响应报文,而不需要额外的比 如“Payload中的数据是管理数据还是业务数据”这个信息。我们在逻辑闭包之内构造了一 个自恰的概念空间,在这个空间中,我们不需要额外的信息就可以进行各种推演,并得到 一些演绎的结论。 我们引入逻辑闭包的概念,主要是解决\ :doc:`../道德经直译/恍惚`\ 的问题。我们在 一个逻辑空间里面讨论问题,我们会用到这个逻辑空间的概念,但我们直接使用那个概念 的结论,我们没有深入那个概念本身。所以那个概念的细节对我们来说,就是一个“恍惚” ,那我们就要知道我们对这个恍惚做了什么假设。而这个假设本身,会成为我们是否信任 那个恍惚的一个推演的需求。 比如前面这个例子中的链路层逻辑闭包,在这个逻辑闭包中,我们直接使用了Payload这 个概念,但我们不关心Payload的内容,也不关心报文的格式是什么。在我们的逻辑链中 ,无论报文的格式是什么,我们的逻辑都可以成立。我们只要知道Node,知道报文的可以 在物理层上发送,我们就可以决定重传,决定报文校验,这就足以支持链路层不丢包。我 们可以在这个闭包的信息范围内挑破绽,把各种有可能出问题的逻辑破绽都填补了。这个 闭包本身就可以成为一个恍惚,我们就可以直接使用比如“协议层通过链路层无丢失地发 送消息给其他节点”这个概念,当我们在协议层讨论这个问题的时候,链路层就是一个恍 惚,我们不进入它的细节,但我们知道它能保证不丢包。 但如果我们在逻辑闭包中引入了细节,这个逻辑可以被应用的范围就小了。比如在前面这 个链路层闭包中,我们认知了Payload的格式,或者我们认为我们知道每条链路都只有1到 2跳。在我们把它认作是恍惚的时候,我们就不知道细节逻辑是否依靠了这些信息。那我 就只能认为这个链路层就只能是固定Payload格式的,只能是一跳或者两跳的。协议层要 用3跳的网络,这个链路层闭包就要打开重新推演。或者你要把协议层和链路层的概念放 到一起去推演,这个公共的闭包包含的概念就会多很多,你的逻辑就会变得非常复杂,甚 至复杂到你的大脑无法判断它是否严密的地步。 很多工程师不希望基于逻辑闭包的抽象概念来考虑问题,是因为抽象思考的难度是大很多 的,一个函数的输入大部分都是常量,肯定比大部分都是变量容易写得多。变量就是一种 对“很多情形”的一个闭包。比如我们用字长为128,Lane长度为8写一个向量乘法的算法, 这是比较好写的,但如果用字长为xlen,lane长度为vl写一个向量乘法的算法,那就复杂 得多了。但显然后者的适用范围也大得多。一旦我们把这个乘法推演通了,无论我们用什 么字长,我们都可以放心用这个算法。所以这是个平衡的问题,但很多时候你要解决的问 题域有那么大,你不用抽象,你根本就无法解决你整个问题域的问题。 从逻辑闭包的角度看待一个逻辑设计,反过来还能让我们判断一个逻辑闭包的结论范围。 比如你的链路层逻辑闭包没有包含QoS分类信息,那你在这个闭包内就不可能进行包调度。 清晰定义逻辑闭包的范围,也让我们知道这个名称黑盒可以作用的范围。我经常看到有设 计师为构架而构架会尝试设计“通用软硬件接口”,或者“万能模块间消息接口”这样的东西 。从逻辑闭包的角度来看这个问题,一个逻辑空间没有约束就没有信息(本质上是约束) ,没有信息就不会产生衍生逻辑,没有衍生逻辑,这个逻辑空间就没有意义。所以,“通用 软硬件接口”这个需求本身毫无意义,你首先要给定你的约束,你才会有建立一个独立逻辑 闭包的驱动力。你的“因为”,“所以”都需要这些基本约束作为根。 说到底,“逻辑闭包”是概念空间的另一个说法。因为实现“逻辑闭包”是我们建立一个概念 空间的基本要求。否则我们随便建逻辑链就可以了,分成一个个独立“空间”干什么?我们 强调逻辑闭包,只是强调建立概念空间在收缩信息范围上的那个要求而已。 抽象概念定义 ============ 要能做好逻辑闭包的设计,我们必须先要学会严肃的抽象概念的定义。抽象定义的是一个 范围,比如1,是一个具象,我们对它的取值有确切的理解。而“自然数”是一个抽象,它 有众多的取值,1,2,3,4,......都是它的取值。当然,这是一个相对的概念。严格来 说1也是一个抽象的概念,因为自然界只存在“一个苹果”,“一片树林”这样的“具象”,一 这是对这些东西某个特定属性的一个“抽象”而已。但一般来说,我们可以在特定的上下文 中上找到一个共识,确定什么是抽象,什么是具象。 当我们定义一个抽象,我们会有两个要求,第一,你必须给出明确的问题域,也就是你为 什么要讨论这个问题。因为抽象本质上是一种“分类”,我们提出自然数这个概念,就是要 区分具有特定性质的东西,区分不具有这种特质的东西(比如小数)。你没有目的地说一 个概念,这个概念没有正确与否可言。 给出明确的问题域,你的逻辑就必须穷举它的所有可能性。你定义一个逻辑,只包含它一 个子集,其他部分当看不见,我们讨论啥?这种“部分成立”的逻辑,就构不成抽象。 第二个问题是你必须对它包含的具象有明确的理解。这其实仍是前面这个穷举可能性的要 求。你要穷举可能性,必然是用一个个的逻辑闭包分隔你的所有可能性,否则只能是一个 “差不多”的东西,它的结果没法用。 今天我评审一份设计,里面有这样一个描述: | Master执行Slave.call命令,Slave从Idle状态变为Running状态。 | Slave在执行中遇到halt指令,跳回Idle状态,Master从Slave.call命令 | 继续执行…… 这个逻辑空间混合了Master和Slave的执行行为和Slave的状态变迁。我第一个反应是考虑 Slave的状态机到底是个独立的闭包,还是属于当前逻辑定义上下文的闭包。是Master执 行需要Slave的状态作为基础呢?还是Slave有一个完整的状态机管理,并用这个状态机的 状态结果来控制Master的行为呢?这里的描述既没有构成Master对Slave状态机的一个“要 求”,也没有提前推演Slave的状态机(让它形成一个闭包,这也可以通过索引其他定义决 定),这段描述的信息熵就非常低。 这个例子和我们写程序很像,你写一个程序,循环打印hello world。你的程序这样写: .. code:: python def print(str): f = open(stdout) for c in str: f = write(c) f.close() def loop_print_hello_world(n): for i in range(0, n): print("hello world") 这里print和loop_print_hello_world就各自构成了一个独立的逻辑闭包,因为它的行为 在它们内部是完全自恰的。loop_print_hello_world()的逻辑链中使用了一个完全封闭的 print的概念,就算print修改成用putc()来实现,不用for循环改而使用递归……这些逻辑 变化,都不改变loop_print_hello_world的逻辑。但如果你的程序是这样写的: .. code:: python i=0 f=0 def print(str): i++ for c in str: f = write(c) f.close() def loop_print_hello_world(n): while i=0) handle_timeout() } receive_process(queue_id) { while true: ptr = dequeue(qeueue_id, &size) if ptr: ret = read_data(ptr) free_communication_memory(queue_id, ptr) if ret != STOP break; } 这里我确切定义了发送方和接收方是两个process,同时声明queue_id是双方约定的。同 时,我对内存分配的概念就是指用户进程可以直接访问的内存的分配。它怎么分配的我不 管,但我对它有确切的要求,就是我拿到它了以后,我是当普通内存那样来访问的。我不 知道它是不是可以实现,但我知道这是我的要求,你后面做具体设计的时候,你就好好告 诉我,你怎么给我queue id,你怎么保证我在一个进程中分配的内存,可以在另一个进程 中释放,这就好了。我给定的一组“名”,是有确切的要求的。你后面怎么打开它,我也是 有确切的要求的。这样的模型就可以一路谈下去。否则你一路描述名字都是模模糊糊的, 不但细节模模糊糊,属性也是模模糊糊,这种“文字”就无法构成逻辑闭包了。 逻辑空间交叉 ------------- 还是上面这个设计,对于这个enqueue,如果我们做成一条指令,有人会这样描述这条指 令::: queuePush 发送方释放内存块,指令将发送方写完的内存块放回池中, 并取消发送方访问它的权限。 这句话我完全不知道它什么意思。你这是一条指令啊,指令能干什么?指令只能修改CPU 的状态,导致CPU对外发出特定的信号(比如产生一个中断,发起一次内存操作等),你 说CPU释放内存块,这句话应该怎么理解? “释放内存块”这个描述应该属于的逻辑空间应该是软件API的上下文啊,你定义一个指令 的行为,用一个软件API的概念是什么意思?是说指令会触发软件函数的调用吗?调用一 个指令导致一个软件行为的发生,这到底应该如何理解? 原设计者的意思可能想说的是queuePush这条支持用于实现enqueue函数,调用后指定的内 存块(假定它有定义)不再可以被发送进程访问。但这个描述同样没有什么信息上的的意 思,因为既然我校验这个指令的行为,我必须确切知道它在CPU这个概念空间中的意义。 比如,这个queuePush指令的行为可能是这样的::: queuePush rd, rs1, rs2 1. phy=当前CPU MMU对应的虚拟地址为rs1的虚拟地址 2. 发消息给CPU ID等于rs2的CPU,要求目标CPU修改其MMU对应页表 的物理地址等于phy的全部映射变更为可读写。如果有多个虚拟 地址,全部变更为可读写 3. 更新当前CPU MMU在rs1虚拟地址的权限,设置为不可访问 这才是指令概念空间里面应该有的描述。 .. vim: set tw=78: