非文本文件存储
没有传统编程所需要的文件目录和文本文件,Unison 的代码是作为 AST 存储在 SQLite 数据库里,比如这条语句:
y = 4 * (2 + x)
当你编译或解释这条语句的时候,它会先进行 词法分析 得到 tokens,然后对 tokens 进行语法分析,最后计算的结果就是这条语句的 AST,表示为:
源码中的一切都能表示为 AST,比如一个函数定义或类型定义,取决于语言,AST 也被用于执行程序中的语句或生产机器码。
Unison 选择以编译后的形式存储代码,它是一种纯静态类型的编程语言,对于曾经玩过 Haskell 或其他 ML-like 的语言(如 OCaml、Standard ML 或 F#)的人来说,它可能看起来很熟悉。
这也是我对它产生兴趣的原因,我想看看 2023 年了,一个非文本文件存储的编程语言是什么样的,要知道这不是一个全新的概念,比如 Smalltalk 也不是基于文本文件的编程语言。
Smalltalk 是 OOP 先驱,特点:
Smalltalk 的部分设计理念放到现在思空见惯了:
Smalltalk 也是另类的:
Unison 看起来是很认真的一次尝试,它将来自 Smalltalk、Haskell、基于 REPL 开发和 Git 版本控制系统混合在一起,成了一个全新的东西
Content-Addressed
使用 Git 时,你可以将对文件的 check-in、check-out 视为对数据库或内容寻址文件系统的操作,同时 Git 跟踪了变更,允许回退或对比 diff
Unison 的工作方式和它差不多,但在函数和类型定义的级别上,Git 管理项目时,它会展示哪些文件已被修改,Unison 使用了一个叫 UCM(Unison Code Manager)的程序跟踪代码变更:
Unison 从 Git 引入了几个重要的概念:
add
指令用于提交变更到 Unison 代码数据库中,并会创建一条历史记录,之后你可以检查历史执行 diff,也能 undo 变更
hash 函数可为任何大小的文件、数据计算出一个摘要或 hash 值,无论文件大小如何,hash 值长度是固定的,Git 使用 SHA1 hash 函数计算出 20 字节长的摘要,原则上,两个不同内容的文件也能计算出相同的 hash 值,但这种可能性很小,就像在地球上随机选择两个位置并捡起同一粒沙子,Unison 使用 SHA3 生成 64 位的摘要
Git 和 Unison 都使用 hash 来唯一命名特定的数据,先看看 Git Blob、Tree 存储结构和 Unix 文件系统的对比:
在 Unix 中,每个文件和目录都有一个唯一标识符,称为 inode,也就是图上的 210、211,在真实的文件系统里,inode 的值往往会比较大。
当向 Git 提交一个文件,它会执行 SHA1 生产出 hash 值作为文件存储的名称,相同的事发生在提交一个 Unison 函数到它的数据库时,Unison 不会提交函数的源代码,而是提交代码的 AST,当创建 AST 时,Unison 将查找你所用的每一个函数、变量和类型的 hash 值,hash 才是函数的真正名称,编辑代码时所用的名称只是一个别名或指针。
假设你写了一个函数 alpha 并调用了之前提交的函数 beta,如果你之后将 beta 重命名为 gamma 也没关系,alpha 的 AST 引用的是 beta 的 hash 而不是名字,你甚至可以将 beta 移动到完全不同的 namespace 或包中,不会产生任何后果,代码仍然可以正常运行:
实际上,影响更深远,你能传递一个函数的 AST 到一个完全不同的计算机上,无论你在哪台计算上计算 hash 值,具有相同代码的函数都会给出相同的 hash 值,因此远程计算机上的 UCM 可以快速确定是否满足所有的依赖关系,并明确地仅向依赖方请求函数。
这意味着可以在分布式环境下对代码进行非常细粒度的控制。
如何修复一个 Unison 函数的 Bug
如果 alpha 函数内部调用了 beta,而 beta 有 bug,需要修改 beta 的代码以修复 bug,但是这会为 beta 产生一个新的 hash 值,这将造成 alpha 函数和其他人仍然指向有问题的旧 beta。
这个问题其实和 Git 一样,当在 Git 里修改并提交一个变动时,旧的 tree 仍然指向的是旧的 blob,Git 解决该问题的方法是从旧的 tree 创建一个副本:
Git 这么做的原因有两个:
最终得到的就是一个新的 commit 指向了一个新的 tree,这正是我们在 Git 中所看到的。它看似浪费了大量空间,但如上图所示,它将尽可能的在新 tree 和老 tree 之间复用 blob。
Unison 的工作模式与之非常像,修改一个函数将生成新的 AST 和 hash,并顺着层级向上递归,每个调用了 beta 函数的函数也将生成一个新的变体,旧的 AST 将保留,虽然可能没有任何指针指向它们:
如果更改函数签名会怎么样
todo
的命令,它会列出待更新的列表没有依赖冲突
通常在其他语言里,如果修改了函数签名,那么类型不匹配时就会导致构建失败,但在 Unison 的设计中 build 是不会失败的,因为它指向的是 hash。具体分两种情况:
菱形依赖不是问题,代码总是可运行,虽然可能存在 bug,但它永远不会构建失败,无论项目有多大:
这是很特别的设计,一个永远不会构建失败的系统
不用等待构建时间
distributed
远程机器上的代码是如何变更的?
分布式数据结构
structural type SimpleTree a
= One a
| Empty
| Two (SimpleTree a) (SimpleTree a)
structural type src.Tree a
= Tree.Two
(distributed.Value (src.Tree a))
(distributed.Value (src.Tree a))
| Empty
| Tree.One (distributed.Value a)
C/S 软件的新形态?
简化部署,不需要构建容器、部署容器 or jarfile 等,直接运行程序,缺失的依赖会自动同步
看看语言本身
工具和语言深度集成
Unison 的代码存储形式