• 什么是 Unison
    • 一家公益组织发起的项目,组织使命:
      • Our overall mission: advance what's possible with software and work to make software creation simpler and more accessible to all. The company does research and development into new, free, open source software technologies like Unison, and builds useful products and services (like the Unison Cloud Platform) to help capitalize this mission.
    • 目前的主要产品是 Unison 和 Unison Cloud:
      • Unison:号称是面向未来的编程语言,content-addressed、distributed
      • UCM:与 Unison 代码交互的接口/工具
      • Unison Cloud:云服务平台,提供基于 Unison 的在线开发、测试、部署和管理等功能
  • Unison 的特征
    • 非文本文件存储

      • 没有传统编程所需要的文件目录和文本文件,Unison 的代码是作为 AST 存储在 SQLite 数据库里,比如这条语句:

        y = 4 * (2 + x)

      • 当你编译或解释这条语句的时候,它会先进行 词法分析 得到 tokens,然后对 tokens 进行语法分析,最后计算的结果就是这条语句的 AST,表示为:

        Untitled

        源码中的一切都能表示为 AST,比如一个函数定义或类型定义,取决于语言,AST 也被用于执行程序中的语句或生产机器码。

      • Unison 选择以编译后的形式存储代码,它是一种纯静态类型的编程语言,对于曾经玩过 Haskell 或其他 ML-like 的语言(如 OCaml、Standard ML 或 F#)的人来说,它可能看起来很熟悉。

        这也是我对它产生兴趣的原因,我想看看 2023 年了,一个非文本文件存储的编程语言是什么样的,要知道这不是一个全新的概念,比如 Smalltalk 也不是基于文本文件的编程语言。

      • Smalltalk 是 OOP 先驱,特点:

        • 一切皆对象,对象之间的通讯只能通过发消息来实现
        • 无原生对象,如 int 或 bool,听起来有些不可思议
        • 无结构控制,如 for 或 switch,听起来有些不可思议
        • 无静态对象,static
      • Smalltalk 的部分设计理念放到现在思空见惯了:

        • 虚拟机,JIT 编译
        • 检查式 debug
        • Hot Reloading
        • 闭包
        • Duck typing
        • MVC、测试驱动开发(TDD)和敏捷开发
      • Smalltalk 也是另类的:

        • 基于 Image 存储而不是基于文件 ← Docker 借鉴了这一概念
        • 面向对象的数据库
      • Unison 看起来是很认真的一次尝试,它将来自 Smalltalk、Haskell、基于 REPL 开发和 Git 版本控制系统混合在一起,成了一个全新的东西

    • Content-Addressed

      • Wiki 定义
      • 假如 Git 是一个 IDE
        • 使用 Git 时,你可以将对文件的 check-in、check-out 视为对数据库或内容寻址文件系统的操作,同时 Git 跟踪了变更,允许回退或对比 diff

        • Unison 的工作方式和它差不多,但在函数和类型定义的级别上,Git 管理项目时,它会展示哪些文件已被修改,Unison 使用了一个叫 UCM(Unison Code Manager)的程序跟踪代码变更:

          Untitled

          • UCM(Unison Code Manager),因为 Unison 代码不是基于文件格式保存的,所以需要一个工具修改和运行 Unison 程序
        • Unison 从 Git 引入了几个重要的概念:

          • Git 可以告诉你修改了哪些文件,而 Unison 会进一步告诉你哪些方法被修改或新增了
          • UCM 的 add 指令用于提交变更到 Unison 代码数据库中,并会创建一条历史记录,之后你可以检查历史执行 diff,也能 undo 变更
            • 该数据库通常位于主目录下的 .unison 子目录中。
          • content-addressed 也是 Unison 从 Git 中引入的概念,和 hash 函数有关:

          Untitled

        • hash 函数可为任何大小的文件、数据计算出一个摘要或 hash 值,无论文件大小如何,hash 值长度是固定的,Git 使用 SHA1 hash 函数计算出 20 字节长的摘要,原则上,两个不同内容的文件也能计算出相同的 hash 值,但这种可能性很小,就像在地球上随机选择两个位置并捡起同一粒沙子,Unison 使用 SHA3 生成 64 位的摘要

        • Git 和 Unison 都使用 hash 来唯一命名特定的数据,先看看 Git Blob、Tree 存储结构和 Unix 文件系统的对比:

          Untitled

          在 Unix 中,每个文件和目录都有一个唯一标识符,称为 inode,也就是图上的 210、211,在真实的文件系统里,inode 的值往往会比较大。

          • 文件的 name 和 path 与 inode 不相关,同一个文件能用不同的 path 来访问
          • inode 相比 hash,它与文件的内容无关,文件内容变化时,inode 不会变,在 Git 中,具有相同内容的对象总是具有相同的名称,而具有不同内容的对象具有不同的名称
        • 当向 Git 提交一个文件,它会执行 SHA1 生产出 hash 值作为文件存储的名称,相同的事发生在提交一个 Unison 函数到它的数据库时,Unison 不会提交函数的源代码,而是提交代码的 AST,当创建 AST 时,Unison 将查找你所用的每一个函数、变量和类型的 hash 值,hash 才是函数的真正名称,编辑代码时所用的名称只是一个别名或指针。

      • 使用 hash 标识函数和类型的影响
        • 假设你写了一个函数 alpha 并调用了之前提交的函数 beta,如果你之后将 beta 重命名为 gamma 也没关系,alpha 的 AST 引用的是 beta 的 hash 而不是名字,你甚至可以将 beta 移动到完全不同的 namespace 或包中,不会产生任何后果,代码仍然可以正常运行:

          Untitled

        • 实际上,影响更深远,你能传递一个函数的 AST 到一个完全不同的计算机上,无论你在哪台计算上计算 hash 值,具有相同代码的函数都会给出相同的 hash 值,因此远程计算机上的 UCM 可以快速确定是否满足所有的依赖关系,并明确地仅向依赖方请求函数。

          这意味着可以在分布式环境下对代码进行非常细粒度的控制。

    • 如何修复一个 Unison 函数的 Bug

      如果 alpha 函数内部调用了 beta,而 beta 有 bug,需要修改 beta 的代码以修复 bug,但是这会为 beta 产生一个新的 hash 值,这将造成 alpha 函数和其他人仍然指向有问题的旧 beta。

      • 这个问题其实和 Git 一样,当在 Git 里修改并提交一个变动时,旧的 tree 仍然指向的是旧的 blob,Git 解决该问题的方法是从旧的 tree 创建一个副本:

        Untitled

      • Git 这么做的原因有两个:

        1. 'B3 是根据修改后的内容创建的新节点,由于它的变动,tree 得向上递归计算出新的 'T2、'T1
        2. Git 中的对象均为不可变对象,只能增加、无法修改
      • 最终得到的就是一个新的 commit 指向了一个新的 tree,这正是我们在 Git 中所看到的。它看似浪费了大量空间,但如上图所示,它将尽可能的在新 tree 和老 tree 之间复用 blob。

      • Unison 的工作模式与之非常像,修改一个函数将生成新的 AST 和 hash,并顺着层级向上递归,每个调用了 beta 函数的函数也将生成一个新的变体,旧的 AST 将保留,虽然可能没有任何指针指向它们:

        Untitled

    • 如果更改函数签名会怎么样

      • 假设 beta 函数原本只有两个入参,修改后变为三个入参,Unison 将识别并将通知所有需要更新的地方,更新后才能调用新的 beta 函数。UCM 有一个名为 todo 的命令,它会列出待更新的列表
    • 没有依赖冲突

      • 通常在其他语言里,如果修改了函数签名,那么类型不匹配时就会导致构建失败,但在 Unison 的设计中 build 是不会失败的,因为它指向的是 hash。具体分两种情况:

        1. Unison 自动更新失败时,现有函数指向旧函数,同时无法更新的位置被添加到 TODO 列表中
        2. 调整旧函数的调用代码,以便允许调用新函数
      • 菱形依赖不是问题,代码总是可运行,虽然可能存在 bug,但它永远不会构建失败,无论项目有多大:

        Untitled

        这是很特别的设计,一个永远不会构建失败的系统

    • 不用等待构建时间

      • 编译流程发生在入库时,词法分析、语法分析、类型检查、优化等只会进行一次,每次写完一个函数,该函数实际上已经编译并将编译后的状态存储在数据库里,也就是说,整个工程随时是完全编译的状态,它不需要有额外的编译系统和编译时间的开销
      • 重命名等重构行为不影响定义,定义作为 hash 永远不会变
      • 依赖不变时,单元测试等测试工具将直接返回缓存结果 ← 和 Image-Based 很像
    • distributed

      Untitled

      • 远程机器上的代码是如何变更的?

        • 远程也很简单,每个变更都会创建一个 patch 描述代码的变更情况,这个 patch 对象能被分发到其他机器上,然后被应用
      • 分布式数据结构

        • 一个二叉树:
        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 的代码存储形式

  • 我怎么看