寫給程序員的可逆計算理論辨析_風聞
canonical-云计算实现计算的云化,可逆计算实现计算的可逆化1小时前
可逆計算理論是Docker、React、Kustomize等一系列基於差量的技術實踐背後存在的統一的軟件構造規律,它的理論內容相對比較抽象,導致一些程序員理解起來存在很多誤解,難以理解這個理論和軟件開發到底有什麼關係,能夠解決哪些實際的軟件開發問題。 在本文中,我將盡量採用程序員熟悉的概念講解差量以及差量合併的概念,並分析一些常見的理解為什麼是錯誤的。
如果對可逆計算理論不瞭解,請先閲讀文章
一. 從Delta差量的角度去理解類繼承
首先,Java的類繼承機制是內置在Java語言中的一種對原有邏輯進行Delta修正的技術手段。例如
public class NopAuthUserBizModel extends CrudBizModel<NopAuthUser> {
@Override protected void defaultPrepareSave(EntityData<NopAuthUser> entityData, IServiceContext context) {
super.defaultPrepareSave(entityData, context);
user.setStatus(NopAuthConstants.USER_STATUS_ACTIVE);
user.setDelFlag(DaoConstants.NO_VALUE);
} }
很多人對可逆的概念都感到很難理解,什麼是差量,到底怎麼逆,逆向執行嗎?這裏説的可逆不是運行期的逆向執行,而是在程序的編譯期所進行的結構變換。比如説我們可以通過類繼承的方式在不修改基類源碼的情況下改變類的行為。
CrudBizModel<?> crud = loadBizModel(“NopAuthUser”); crud.save(entityData);
同樣的CrudBizModel類型,如果實際對應的Java類不同,則執行的業務邏輯就不同。繼承可以看作是向已經存在的基類補充Delta信息。
1. B = A + Delta, **所謂的Delta,就是在不修改A的情況下向A****補充一些信息,**將它轉化為B。
2. 可逆指的是我們可以通過Delta****來刪除基類中已經存在的結構。
當然Java本身並不支持通過繼承來刪除基類中的方法,但是我們可以通過繼承將該函數重載為空函數,然後可以寄希望於運行期的JIT編譯器能夠識別這個情況,從而在JIT編譯結果中完全刪除這個空函數的調用,最終達到完全刪除基類結構的效果。
還可以舉另外一個例子,假設我們已經編寫了一個銀行核心系統,在某個銀行部署的時候客户要求進行定製化開發,要求刪除賬户上某些多餘的字段,同時要增加一些行內業務要用到的定製字段。此時,如果不允許修改基礎產品中的代碼,我們可以採用如下方案
class BankAccountEx extends BankAccount{
String refAccountId;
public String getRefAccountId(){
return refAccountId;
}
public void setRefAccountId(String refAccountId){
this.refAccountId = refAccountId;
} }
我們可以增加一個擴展賬户對象,它從原有的賬户對象繼承,從而具有原有賬户對象的所有字段,然後在擴展對象上我們可以引入擴展字段。然後我們在ORM配置中使用擴展對象,
<entity name=“bank.BankAccount” className=“mybank.BankAccountEx”>…</entity>
以上配置表示保持原有的實體名不變,將實體所對應的Java實體類改成BankAccountExt。這樣的話,如果我們此前編程中創建實體對象的時候都是使用如下方法
BankAccount account = dao.newEntity(); 或者 BankAccount acount = ormTemplate.newEntity(BankAccount.class.getName());
則我們實際創建的實體對象是擴展類對象。而且因為ORM引擎內部知道每個實體類名所對應的具體的實現類,所以通過關聯對象語法加載的所有Account對象也是擴展類型。
BankAccount parentAccount = account.getParent(); // parent返回的是BankAccountEx類型
原有的代碼使用BankAccount類型不需要發生改變,而新寫的代碼如果用到擴展字段,則可以將account強制轉型為BankAccountEx來使用。
關於刪除字段的需求,Java並不支持刪除基類中的字段,我們該怎麼做呢?實際上我們可以通過定製ORM模型來實現某種刪除字段的效果。
<orm x:extends=“super”>
<entity name=“bank.BankAccount” className=“mybank.BankAccountEx” >
<columns>
<column name=“refAccountId” code=“REF_ACCOUNT_ID” sqlType=“VARCHAR” length=“20” />
<column name=“phone3” code=“PHONE3” x:override=“remove” />
</columns>
</entity>
</orm>
根節點上的x:extends=“super"表示繼承基礎產品中的ORM模型文件(如果不寫,則表示新建一個模型,完全放棄此前的配置)。字段phone3上標記了x:override=“remove”,它表示從基礎模型中刪除這個字段。
如果在ORM模型中刪除了字段,則ORM引擎就會忽略Java實體類上的對應字段,不會為它生成建表語句、Insert語句、Update語句等。這樣的話,從實際的效果上説,就達到了刪除字段的效果。
無論如何操作被刪除的字段phone3,我們都觀測不到系統中有任何的變化,而一個無法被觀測、對外部世界也沒有影響的量,我們可以認為它是不存在的。
更進一步的,Nop平台中的GraphQL引擎會自動根據ORM模型來生成GraphQL類型的基類,因此如果ORM模型中刪除了某個字段,則自動的在GraphQL服務中也會自動刪除這個字段,不會為它生成DataLoader。
Trait: 獨立存在的Delta差量
class B extends A是在類A的基礎上補充Delta信息,但是這個Delta是依附於A而存在的,也就是説B中所定義的Delta是隻針對A而實現的,脱離了A的Delta沒有任何意義。 鑑於這種情況,有些程序員可能對”可逆計算理論要求差量獨立存在”這一點感到疑惑,差量不是對base的修改嗎,它怎麼可能脱離base而獨立存在呢?
帶着這個疑問,我們來看一下Scala語言的核心創新之一:Trait機制,關於它的介紹可以參見網上的文章,例如 Scala Trait 詳解(實例)。
trait HasRefId{
var refAccountId:String = null;
def getRefAccountId() = refAccountId;
def setRefAccountId(accountId: String): Unit ={
this.refAccountId = accountId;
}
}
class BankAccountEx extends BankAccount with HasRefId{ }
class BankCardEx extends BankCard with HasRefId{ }
trait HasRefId相當於是一種Delta,它表示為基礎對象增加一個refAccountId屬性,我們在聲明BankAccountEx類時只需要混入這個trait就可以自動實現在BankAccount類的基礎上增加屬性。
需要特別注意的是, HasRefId這個trait是獨立編譯、獨立管理的。也就是説,即使編譯的時候沒有BankAccount對象,HasRefId這個trait也是具有自己的業務含義,可以被分析、存放的。而且我們注意到,同樣的trait可以作用於多個不同的基礎對象,它並不和某個基類綁定。例如上面的BanCardEx也混入了同樣的HasRefId。
在編程的時候,我們也可以針對trait類型編程,不需要使用到任何base對象的信息
def myFunc(acc: HasRefId with HasUserId): Unit = { print(acc.getRefAccountId()); }
上面的函數接收一個參數acc,只要求acc滿足兩個trait的結構要求。
如果從數學的角度上分析一下,我們會發現,類的繼承對應於 B > A , 表示B比A多,但是多出來的東西並沒有辦法被獨立出來。但是Scala的trait相當於是 B = A with C, 這個被明確抽象出來的C可以應用到多個不同的基類上,比如 D = E with C等。在這個意義上,我們當然可以説C是獨立於A或者E而獨立存在的。
Scala的這個trait機制後來被Rust語言繼承併發揚光大,成為這個當紅炸子雞的所謂零開銷抽象的獨門秘技之一。
DeltaJ: 具備刪除語義的Delta差量
從Delta差量的角度看,Scala的trait的功能並不完整,它無法實現刪除字段或者函數的功能。德國的一個教授Shaefer察覺到軟件工程領域中刪除語義的缺乏,提出了一種包含刪除操作的Delta定義方式:DeltaJ語言,並提出了Delta Oriented Programming的概念。
deltaj
詳細介紹可以參見 從可逆計算看Delta Oriented Programming
Delta合併與繼承的區別
可逆計算理論中提出的Delta合併算子類似於繼承概念,但是又有着一些本質性的區別。
傳統的編程理論非常強調封裝性,但是可逆計算是面向演化的,而演化必然是破壞封裝性的。在可逆計算理論中,封裝性並沒有那麼重要,所以Delta合併可以具有刪除語義,並且是把基礎模型作為白盒結構來看待,而不是不可被分析的黑盒對象。Delta合併對封裝性的最終破壞程度由XDef元模型約束來限制,避免它突破最終的形式約束。
繼承會產生新的類名,而原有類型指向的對象結構並不會發生變化。但是根據可逆計算理論設計的Delta定製機制相當於是直接修改模型路徑所對應的模型結構,並不會產生新的模型路徑。例如對於模型文件/bank/orm/app.orm.xml,我們可以在delta目錄下增加一個相同子路徑的文件來覆蓋它,然後在這個文件中再通過x:extends=“super"來繼承原有的模型。所有使用/bank/orm/app.orm.xml的地方實際裝載的將是定製後的模型。
<!-- /_delta/default/bank/orm/app.orm.xml --> <orm x:extends=“super”> … </orm>
因為Delta定製並不會改變模型路徑,所以所有根據模型路徑和對象名建立的概念網絡都不會因為定製而產生扭曲和移動,它保證了定製是一種完全局域化的操作。可以想見,如果是一般的面向對象繼承,在不修改源碼的情況下我們不可能在局部把硬編碼的基類名替換成派生類的類名,這樣就只能擴大重載範圍,比如重載整個函數,替換整個頁面等。很多情況下,我們都無法有效控制局部需求變化的影響範圍,我們還為這種現象起了一個名字:抽象泄露。一旦抽象泄露,就可能出現影響範圍不斷擴大,最終甚至導致架構崩潰。
Delta定製與繼承的第三個區別在於繼承定義在短程關聯之上。面向對象的繼承在結構層面可以被看作是Map之間的覆蓋:每個類相當於是一個Map,它的key是屬性名和方法名。Map是一種典型的短程關聯,它只有容器-元素這樣一級結構關係。而Delta****定製是定義在樹形結構這一典型的長程關聯之上:父節點控制着所有遞歸包含的子節點,如果刪除了父節點,所有遞歸包含的子節點都會被刪除。Delta定製在結構層面可以被看作是樹形結構之間的覆蓋:Tree = Tree x-extends Tree。在後面的章節中,我會在理論層面解釋樹形結構相比於Map結構的優勢之處。
二. Docker作為可逆計算理論的實例
可逆計算理論指出,在圖靈機理論和Lambda演算理論之外,存在着第三條通向圖靈完備的中間路徑,我們可以用一個公式來表達這條技術路線
App = Delta x-extends Generator<DSL>
• x-extends是一個詞,它表示對面向對象的extends機制的一種擴展。有些人可能把它誤認為x減去extends,結果導致非常困惑。
• Generator<DSL>是一種類似泛型的寫法,它表示Generator採用類似C++模板元編程的技術,在編譯期將DSL作為數據對象進行加工轉換,動態生成Delta所將要覆蓋的基類。
一個複雜的結構化的類型聲明如果進一步引入執行語義就會自動成為DSL(Domain Specific Language),所以Generator相當於是一個模板宏函數,它接受一個類似類型定義的DSL,在編譯期動態生成一個基類。
Docker鏡像的整體構造模式可以看作是
App = DockerBuild<DockerFile> overlay-fs BaseImage
DockerFile就是一種DSL語言,而Docker鏡像的build工具相當於是一種生成器,它解釋DockerFile中定義的apt install等DSL語句,動態將它們展開為硬盤上對文件系統的一種差量化修改(新建文件、修改文件、刪除文件等)。
OverlayFS 是一種堆疊文件系統,它依賴並建立在其它的文件系統之上(例如 ext4fs 和 xfs 等等),並不直接參與磁盤空間結構的劃分,僅僅將原來底層文件系統中不同的目錄進行 “合併”,然後向用户呈現,這也就是聯合掛載技術。OverlayFS在查找文件的時候會先在上層找,找不到的情況下再到下層找。如果需要列舉文件夾內的所有文件,則會合並上層目錄和下層目錄的所有文件統一返回。如果用Java語言實現一種類似OverlayFS的虛擬文件系統,結果代碼就類似於Nop平台中的DeltaResourceStore。OverlayFS的這種合併過程就是一種標準的樹狀結構差量合併過程,特別是我們可以通過增加一個Whiteout文件來表示刪除一個文件或者目錄,所以它符合x-extends算子的要求。
Docker鏡像與虛擬機增量備份的對比
有些程序員對於Docker技術存在誤解,認為它就是一種輕量級的虛擬化封裝技術或者説是一種很方便的應用打包工具,與Delta差量有什麼關係呢?當然,從使用層面上説Docker確實相當於是一種輕量級的虛擬機,但關鍵是它是憑藉什麼技術實現輕量級的?它作為一種打包工具與其他的打包工具相比有什麼本質上的優勢?
在Docker之前,虛擬機技術就可以實現增量備份,但是虛擬機的增量是定義在字節空間中,虛擬機的增量文件在脱離了基礎鏡像的情況下是沒有業務含義的,也不能為獨立的被構造、獨立的被管理。而Docker則不同,Docker鏡像的Delta差量是定義在文件系統空間中,所謂的Delta****的最小單位不是字節而是文件。比如説,如果我們現在有一個10M的文件,如果我們為這個文件增加一個字節,則鏡像會增大10M,因為OverlayFS要經歷一個copy up過程,將下層的整個文件拷貝到上層,然後再在上層進行修改。
在Docker所定義的差量空間中,半個文件A+半個文件B不是一個合法定義的差量,我們也不可能用Docker的工具構造出這樣一個差量鏡像出來。所有的鏡像中包含的都是完整的文件,它的特殊性在於還包含某種負文件。例如,某個鏡像可以表示(+A,-B),增加文件A,同時刪除文件B,而修改文件的某個部分這一概念無法被直接表達,它將被替換為增加文件A2。A2對應於修改後產生的結果文件。
Docker鏡像是獨立於基礎鏡像存在的Delta差量,我們可以在完全不下載基礎鏡像的情況下獨立的製作一個應用鏡像。實際上Docker鏡像就是一個tar文件,用zip工具打開之後我們會看到每一層都對應於一個目錄,我們只要通過拷貝操作將文件拷貝到對應目錄中,然後計算hash碼,生成元數據,再調用tar打包就可以生成一個鏡像文件了。對比虛擬機的字節空間中的差量備份文件,我們缺少合適的、針對虛擬機字節空間的、穩定可靠的技術操作手段。而在文件系統空間中,所有的生成、轉換、刪除文件的命令行程序都自動成為這個Delta差量空間中的變換算子。
在數學上,不同的數學結構空間中它們允許存在的算子的豐富性是不同的。如果是一個貧瘠的結構空間,例如字節空間,那麼我們就缺少一些強大的結構變換手段。
Docker鏡像與Git版本的對比
有些同學可能會疑惑,Docker的這種差量是否與Git是類似的?確實,Git****也是一種差量管理技術,但是它所管理的差量是定義在文本行空間中的。這兩種技術的區別從可逆計算的角度去看,只在於它們對應的差量空間不同,進而導致這兩個空間中存在的算子(Operator)不同。Docker所對應的文件系統空間可用的算子特別多,每個命令行工具都自動成為這個差量空間上的合法操作。而在文本行空間,如果我們隨便操作,很容易就導致產生的源碼文件語法結構出現混亂,無法通過編譯。因此,在Docker的差量空間中,我們有很多的Generator可以生成Delta,而在Git的差量空間中,所有的變更操作都是由人手工完成的,而不是由某個程序自動生成的。比如説我們要給某個表增加字段A,我們是手工修改源碼文件,而不是期待通過某個Git集成的工具自動對源碼進行修改。有趣的一點是,如果我們為Git配備一個結構化的比較與合併工具,比如集成Nop平台的Delta合併工具等,則可以將Git所面對的Delta空間修改為DSL所在的領域模型空間,而不再是文本行空間。在領域模型空間中,通過Delta合併算子進行的變換可以保證結果格式一定是合法的XML格式,而且所有的屬性名、節點名都是XDef元模型文件中定義的合法名稱。
再次強調一下,差量概念是否有用關鍵在於它到底定義在哪個差量模型空間中,在這個空間中我們能夠建立多少有用的差量運算關係。在一個貧瘠的結構空間中定義的差量並沒有多大的價值,差量與差量並不是生而平等的。
有些人可能懷疑Delta差量是不是就是給一個json加上版本號,然後用新版本替換舊版本?問題沒有這麼簡單。差量化處理首先要定義差量所在的空間。在Nop平台中差量是定義在領域模型空間,而不是文件空間中的,不是説整個JSON文件加個版本號,然後將整個JSON文件替換為新的版本,而是在文件內部我們可以對每一個節點、每一個屬性進行單獨的差量定製,也就是説在Nop平台的差量空間中,每一個最小的元素都是具有領域語義的業務層面上穩定的概念。另外一個區別在於,根據XDSL規範,Nop平台中所有的領域模型都支持x:gen-extends和x:post-extends編譯期元編程機制,可以在編譯期動態生成領域模型,然後再進行差量合併。這樣整體上可以滿足 DSL = Delta x-extends Geneator<DSL0>的計算範式要求。很顯然,一般的JSON相關技術,包括JSON Patch技術並沒有內置的Generator的概念。
三. Delta定製的理論和實踐意義
基於可逆計算理論的指導,Nop平台在實操中可以做到的效果就是: 一個複雜的銀行核心應用可以在完全不修改基礎產品源碼的情況下,通過Delta****定製進行定製化開發,為特定的銀行實現完全定製的數據結構、後台邏輯、前端界面等。
從軟件工程的角度去理解,可逆計算理論解決了粗粒度的系統級軟件複用的問題,即我們可以複用整個軟件系統,不需要把它拆解為分立的模塊、組件。組件技術在理論方面存在缺陷,所謂的組件複用是相同可以複用,我們複用的是A和B之間的公共的部分,但是A和B的公共部分是比A和B都要小的,這直接導致組件技術的複用粒度無法擴展到比較宏觀的層次。因為一個東西的粒度越大就越難找到和它完全一樣的東西。
可逆計算理論指出 X = A + B + C, Y = A + B + D = X + (-C + D) = X + Delta,在引入逆元的情況下,任意的X和任意的Y都可以建立運算關係,從而在不修改X的情況下,可以通過補充Delta信息實現對X的複用。也就是説,可逆計算理論將軟件的複用原理從**”****相同可複用”****擴展到了”**相關可複用”。組件的複用是基於整體-部分之間的組合關係,而可逆計算指出對象之間除了組合關係之外還可以建立更靈活的轉化關係。
Y = X + Delta, 在可逆計算的視角下, Y 是在X上補充Delta信息得到的,但是它可能比X更小,而不是説它一定比X要大,這個觀點和組件理論有着本質性區別。想象一下 Y = X + (- C) 表示從X中刪除一個C得到Y。實際得到的Y是比X更小的結構。
美國的卡內基梅隆大學軟件工程研究所是軟件工程領域的權威機構,它們提出了一個所謂的軟件產品線工程理論,指出軟件工程的發展歷程就是不斷提升軟件複用度,從函數級複用、對象級複用、組件級複用、模塊級複用,最終實現系統級複用的發展歷程。軟件產品線理論試圖為軟件的工業化生產建立理論基礎,可以像工業生產線一樣源源不斷的生成軟件產品,但是它並沒有能夠找到一種很好的技術手段能夠以很低廉的成本實現系統級複用。軟件產品線傳統的構建方式要使用到類似C語言的宏開關的機制,維護成本很高。而可逆計算理論相當於是為落實軟件產品線工程的技術目標提供了一條可行的技術路線。具體分析可以參見 從可逆計算看Delta Oriented Programming
為了具體説明如何進行Delta定製,我提供了一個示例工程 nop-app-mall, 介紹文章 如何在不修改基礎產品源碼的情況下實現定製化開發 演示視頻 B站
Delta定製與插件化的區別
有些程序員一直有疑問,我們傳統的”正交分解”、“模塊化”、“插件系統”按照功能進行聚類,將相關功能集合為一個庫、包等,不也能實現複用嗎?可逆計算的複用有什麼特殊之處?差別就在於複用的粒度不同。傳統的複用方式無法實現系統級別的複用。想象一下,系統中包含1000多個頁面,某個客户説我要在頁面A上增加按鈕B,刪除按鈕C,使用傳統的複用技術怎麼做?為每一個按鈕都寫一個運行時控制開關嗎?如果不修改頁面對應的源碼,能實現客户的需求嗎?如果後來基礎產品升級了,它在前台頁面中增加了一個新的快捷鍵操作方式,我們的定製代碼是否能夠自動繼承基礎產品已經實現的功能,還是必須由程序員手工進行代碼合併?
傳統的可擴展技術依賴於我們對未來變化點的可靠預測,比如插件系統我們必須要定義插件到底掛接在哪些擴展點。但現實情況是,我們不可能把系統中所有可能擴展的地方都設計成擴展點,比如説我們很難為每個按鈕的每個屬性都設計一個開關控制變量。缺少細粒度的開關很容易導致我們的擴展粒度因為技術受限而變大,比如客户只是要修改一下某個界面上的某個字段的顯示控件,結果我們必須要定製整個頁面,本來是一個字段級別的定製問題因為系統缺少靈活定製的能力而不得不上升為頁面級別的定製問題。
K8s在1.14版本之後力推所謂的Kustomize聲明式配置管理技術,這是為了解決類似的可擴展性問題而發明的一種解決方案,它可以被看作是可逆計算理論的一個應用實例,而且基於可逆計算理論我們還很容易的看出Kustomize技術未來可能的改進方向。具體參見 從可逆計算看kustomize
Delta差量與數據差量處理的關係
Delta差量的思想其實在數據處理領域並不罕見。比如説
1. 數據存儲領域使用的LSM樹(Log-Structured-Merge-Tree),它就相當於是按照分層的方式進行差量管理,每次查詢數據的時候都會檢查所有的層,合併差量運算結果之後返回給調用者。而LSM樹的壓縮操作可以看作是對Delta進行合併運算的過程。
2. MapReduce算法中的Map端Combiner優化可以看作是利用運算的結合律對Delta差量進行了預合併,從而減輕Reduce階段的負擔
3. 事件溯源(Event Sourceing)架構模式中我們將針對某個對象的修改歷史記錄下來,然後在查詢當前狀態數據時通過Aggregate聚合操作合併所有Delta修改記錄,得到最終結果。
4. 大數據領域中目前最熱門的所謂流批一體,流表二象性(Stream Table Duality)。我們對錶的修改將會通過binlog成為Delta變更數據流,而把這些Delta合併在一起得到的快照就是所謂的動態表。
5. 數倉領域的Apache Doris內置了所謂的Aggregate數據模型,在導入數據的時候就執行Delta差量預合併計算,從而極大的減輕查詢時的計算量。而DataBricks公司直接把它的數據湖技術核心命名為Delta Lake,在存儲層直接支持增量數據處理。
6. 甚至在前端編程領域,所謂的Redux框架,它的具體做法也就是把一個個的action看作是對State的差量化變更,通過記錄所有這些Delta實現時光旅行。
程序員們現在已經習慣了不可變數據的概念,因此在不可變數據的基礎上發生的變化的數據很自然的就成為了Delta。但是正如我在此前的文章中指出的,數據和函數是對偶的關係,數據可以看作是作用於函數之上的泛函(函數的函數),我們同樣需要建立不可變邏輯的概念。如果我們把代碼看作是邏輯的一種資源化表示,那麼我們應該也可以對邏輯結構進行Delta修正。大部分程序員現在並沒有意識到邏輯結構也是像數據一樣可以被程序操縱,並通過Delta修正來調整的。 Lisp語言雖然很早就確立了“代碼即數據”的設計思想,但是它並沒有進一步提出一種系統化的支持可逆差量運算的技術方案。
在軟件領域的實踐中,Delta、差量、可逆等概念的應用正越來越多,在5到10****年內,我們可以期待整個業界發生一次從全量到差量的概念範式轉換,我願將它稱之為差量革命。
有趣的是,在深度學習領域,可逆、殘差連接等概念已經成為標準理論的一部分,而神經網絡的每一層結構都可以看作是 Y = F(X) + Delta這樣一種計算模式。
四. 可逆計算理論的概念辨析什麼是領域模型座標系
我在介紹可逆計算理論的時候會反覆提及領域模型座標系的概念,Delta差量的獨立存在隱含的要求領域座標的穩定存在。那麼,什麼是領域座標?一般的程序員所接觸到的座標只有平面座標、三維座標等,可能對於抽象的、數學意義上的座標概念感到難以理解。下面,我將詳細解釋一下可逆計算理論中的領域座標概念到底包含什麼內容,它的引入又會給我們的世界觀造成什麼不一樣的影響。
首先,在可逆計算理論中我們談到座標,指的是存取值的時候所使用的某種唯一標識,對於任何支持如下兩個運算的唯一標識,我們都可以認為它是一個座標:
1. value = get(path)
2. set(path, value)
而所謂的一個座標系統,就是為系統中涉及到的每一個值都賦予一個唯一的座標。
具體來説,對於如下的一個XML結構,我們可以將它展平後寫成一個Map形式
<entity name=“MyEntity” table=“MY_ENTITY”> <columns> <column name=“status” sqlType=“VARCHAR” lenght=“10” /> </columns> </entity>
對應於
{ “/@name”: “MyEntity”, “/@table”: “MY_ENTITY”, “/columns/column[@name=‘status’]/@name”: “status”, “/columns/column[@name=‘status’]/@sqlType”: “VARCHAR” “/columns/column[@name=‘status’]/@length”: 10 }
每一個屬性值都有一個唯一的對應的XPath可以直接定位到它。通過調用get(rootNode, xpath)我們可以讀取到對應屬性的值。 在MangoDB這種支持JSON格式字段的數據庫中,JSON對象實際上就是被展平成類似的Map結構來存儲,從而可以為JSON對象中的值建立索引。只不過JSON對象的索引中使用的是JSON Path而不是XPath。這裏的XPath就是我們所謂的領域座標。
XPath規範中規定的XPath具備匹配多個節點的能力,但是在Nop平台中我們只使用具有唯一定位功能的XPath,而且對於集合元素我們只支持根據唯一鍵字段來定位子元素。
對於上面的Map結構,我們也可以把它簡寫為多維向量的形式:
[‘MyEntity’,‘MY_ENTITY’, ‘status’,‘VARCHAR’,10]
我們只需要記住這個向量的第一個維度對應於/@name處的值,而第二個維度對應於/@table處的值,依此類推。
可以想象一下,所有可能的DSL所構成的座標系實際上是一個無限維的向量空間。例如,一個列表中可以增加任意多條子元素,那麼對應到領域座標系的向量表示中就可能對應無窮多個不同的變化維度。
如果把DSL模型對象看作是定義了一個領域語義空間,那麼DSL描述中的每個值現在就是在這個語義空間中的某個位置處的值,而這個位置所對應的座標就是XPath,它的每個部分都是領域內部有意義的概念,因為整個XPath在領域語義空間中也具有明確的業務含義,所以我們將它簡稱為領域座標,強調它是在領域語義空間中具有領域含義的座標表示。與此相反,Git Diff中我們定位差異時使用的座標是哪個文件的哪一行,這個座標表示是與具體業務領域中的領域概念完全無關的,因此我們説Git所使用的Delta差量空間不是領域語義空間,它所使用的定位標識也不是領域座標。
在物理學中,當我們為相空間中的每一點都指定一個座標以後,就從牛頓力學的基於質點的世界觀轉向了所謂的場論的世界觀。後續電動力學、相對論、量子力學的發展所採用的都是場論的世界觀。簡單的説,在場論的世界觀下,我們關注的重點不再是單個對象怎麼和其他對象發生相互作用,而是在一個無所不在的座標系中觀察對象上的屬性值如何在給定座標點處發生變化。
基於領域座標系的概念,無論業務邏輯如何發展,我們描述業務所用的DSL對象在領域座標系中一定是具有唯一的表示的。比如最初DSL對應的表示是 [‘MyEntity’,‘MY_ENTITY’, ‘status’,‘VARCHAR’,10],後來演化成了 [‘MyEntity’,‘MY_ENTITY’, ‘status’,‘VARCHAR’,20], 這個20所對應的座標是 “/columns/column[@name=‘status’]/@length”,因此它表示我們將status字段的長度值調整到了20。
當我們需要對已有的系統進行定製的時候,只需要在領域模型向量中找到對應的位置,直接修改它的值就可以了。這就類似於我們在一個平面上根據x-y座標找到對應的點,然後修改這個位置處的值。這種定製方式完全不依賴於系統內部是否已經內置了某種擴展接口、插件體系。因為所有的業務邏輯都是在領域座標系中進行定義的,所有的業務邏輯變化都是建立在領域座標基礎上的一個Delta差量。
Delta合併滿足結合律的證明
函數式編程語言中有一個出身很高貴的概念:Monad,是否理解這個概念是判斷函數式愛好者是否已經入門的標誌性事件。Monad從抽象數學的角度去理解,基本對應於數學和物理學中的半羣概念,即具有單位元且滿足結合律。所謂的結合律指的是運算關係的結合順序不影響最終結果:
a + b + c = (a + b) + c = a + (b + c)
這裏的運算關係採用加號來表示有些誤導,因為加法滿足交換律(a + b = b + a),但是一般的結合運算並不需要滿足交換律,比如函數之間的複合關係就滿足結合律,但是一般情況下f(g(x))並不等價於g(f(x))。為了避免誤解,下面我會使用符號⊕ 來表示兩個量之間的結合運算關係。
關於Monad的知識,可以參考我的文章 寫給小白的Monad指北。曾有網友反映這是全網最通俗易懂的關於State Monad的介紹。
首先,我們可以證明:如果一個向量的每個維度都滿足結合律,則整個向量之間的運算也滿足結合律。
([A1, A2] ⊕ [B1,B2]) ⊕ [C1,C2] = [A1 ⊕ B1, A2 ⊕ B2] ⊕ [C1,C2]
= [(A1 ⊕ B1) ⊕ C1, (A2 ⊕ B2) ⊕ C2]
= [A1 ⊕ (B1 ⊕ C1), A2 ⊕ (B2 ⊕ C2)]
= [A1, A2] ⊕ ([B1, B2] ⊕ [C1,C2])
考慮到上一節中我們對於領域座標系的定義,為了證明Delta合併滿足結合律,我們只需要證明在單個座標處的合併運算滿足結合律即可。
最簡單的情況是我們常見的覆蓋更新:每次運算都是用後面的值覆蓋前面的值。我們可以選擇用一個特殊的值來表示刪除,這樣的話就可以將刪除也納入到覆蓋更新的情況中來。如果後面的值表示刪除,則無論前面的值是什麼,最終的結果都是刪除。數據庫領域的BinLog機制其實就是採用的這種做法:每次對數據庫行的修改都會產生一條變更記錄,變更記錄中記錄了行的最新的值,只要接收到變更記錄,我們就可以放棄此前的值。在數學上它對應於 A ⊕ B = B ,顯然
(A ⊕ B) ⊕ C = B ⊕ C = C = B ⊕ C = A ⊕ (B ⊕ C)
覆蓋操作是滿足結合律的。
另外一個稍微複雜一些的結合運算是類似AOP的運算,我們可以在基礎結構的前面和後面追加一些內容。
B = a super b, C = c super d
B通過super引用基礎結構,然後在基礎結構的前面增加a, 而在後面增加b
(A ⊕ B) ⊕ C = (a A b) ⊕ C = c a A b d = A ⊕ ( c a super b d) = A ⊕ (B ⊕ C)
以上就證明了Delta合併運算是滿足結合律的。
如何理解差量是獨立的
有些程序員對於Delta差量是獨立存在的這一概念始終感到費解,難道刪除操作能獨立於基礎結構存在嗎?如果基礎表上壓根沒有這個字段,刪除字段不就報錯了嗎?如果一個Delta表示修改基礎表中某個字段的類型,難道它能獨立於基礎表存在嗎?如果將它應用到一個壓根就沒有這個字段的表上,不就報錯了嗎?
出現這種疑問很正常,因為作為逆元存在的負差量是很難理解的。在科學領域,對於負數的認知也是很晚近的事情。連17世紀微積分的發明人萊布尼茲都在信件中抱怨負數的邏輯基礎不牢靠。參見 負數簡史:承認負數是一次思想的飛躍
為了認識這一概念,我們首先要區分抽象的邏輯世界,以及我們真實所在的物理世界。在抽象的邏輯世界中,我們可以承認如下定義合法:
表A(增加字段A,修改字段B的類型為VARCHAR,刪除字段C)
即使表A上沒有字段B和字段C也不會影響這個定義的合法性。如果接受了這一點,我們就可以證明在表A上應用任何差量運算得到的結果都是這個空間中的合法存在的一個元素(這在數學上稱為是封閉律)。
在完全不考慮表A中具有什麼字段的前提下,我們在邏輯空間中可以合併多個對錶A進行操作的Delta,例如
表A(增加字段A,修改字段B的類型為VARCHAR,刪除字段C) + 表A(_,修改字段B的類型為INTEGER,_) = 表A(增加字段A,修改字段B的類型為INTEGER,刪除字段C)
這種做法有些類似於函數式語言中的延遲處理。函數式語言中 range(0, Infinity).take(5).take(2)的第一步就無法執行,但實際上take(5)和take(2)可以無視這一點,先行復合在一起,然後再作用到range(0,Infinity)上得到有限的結果。
在一個存在單位元的差量化空間中,全量可以被看作是差量的特例,例如
表A(字段A,字段B) = 空 + 表A(增加字段A,增加字段B)
那麼如何解決在現實中我們無法從不存在字段C的表中執行刪除字段C的操作這一難題呢?答案很簡單,我們引入一個觀測投影算符,規定從邏輯空間投影到物理空間中時自動刪除所有不存在的字段。例如
表A(增加字段A, 修改字段B的類型為VARCHAR, 刪除字段C) -> 表A(字段A)
也就是説,如果是修改或者刪除操作,但是表A上沒有對應的字段,則可以直接忽略這個操作。
這種説法聽起來可能有些抽象。具體在Nop平台中的做法如下:
<entity name=“test.MyEntity”> <columns> <column name=“fieldB” sqlType=“VARCHAR” x:virtual=“true” /> <column name=“fieldC” x:override=“remove” /> </columns> </entity>
Nop平台中的Delta合併算法規定,所有的差量合併完畢之後,檢查所有具有x:override=“remove"屬性的節點,自動刪除這些節點。另外,也檢查所有具有x:virtual=“true"的節點,因為合併過程中只要覆蓋到基礎節點上,就會自動刪除x:virtual屬性,所以如果最後仍然保留了x:virtual屬性,就表明合併過程中最終也沒有在基礎模型中找到對應的節點,那麼這些節點也會被自動刪除。
差量運算的結果所張成的空間是一個很大的空間,我們可以認為它是所有可行的運算所導致的結果空間。但是我們實際能夠觀測到的物理世界僅僅是這個可行的空間的一個投影結果。這個視角有些類似於量子力學中的波包塌縮的概念:量子態的演化是在一個抽象的數學空間中,但是我們所觀測的所有物理事實是波包塌縮以後的結果,也就是説在數學空間中薛定諤的貓可以處在既死又活的量子疊加態中,但是我們實際觀測到的物理結果只能是貓死了或者貓活着。
基於可逆計算理論設計的低代碼平台NopPlatform已開源:
• gitee: canonical-entropy/nop-entropy
• github: entropy-cloud/nop-entropy
• 開發示例:docs/tutorial/tutorial.md
• 可逆計算原理和Nop平台介紹及答疑_嗶哩嗶哩_bilibili