关注了就能看到更多这么棒的文章哦~
Programming in Unison
By Daroc Alden
June 25, 2024
Gemini-1.5-flash translation
https://lwn.net/Articles/978955/
Unison 是一种使用 MIT 许可的编程语言,自 2013 年以来一直在开发中,它探索了将代码设为不可变并存储在数据库中而不是一组文本文件中的影响。Unison 支持一种大大简化的分布式编程模型——它用与程序本身相同的语言描述程序的配置和通信。在此过程中,它引入了一种新的与编程语言交互的方法,是专门针对 Unison 语言设计的。
每种编程语言,尤其是刚起步的语言,都需要一个细分市场。Unison 选择的细分市场是云计算——通过从根本上简化现有技术的一些让人不太舒服的地方,使其更容易构建现代分布式系统。虽然使用该语言将简单的本地脚本组合在一起肯定也是可能的,但核心开发人员的重点是使分布式系统和基于 Web 的应用程序的开发尽可能地无缝进行。为了支持这一使命,该语言采用了若干不寻常的功能。
命名
使该语言从根本上与众不同的地方就是代码的存储方式。与大多数将程序存储为文本的其他编程语言不同,Unison 将程序存储为机器可读的格式。还有其他语言也这样做,包括像 Smalltalk 这样的使用基于映像文件的持久性的语言(image-based persistence),或者像 LabVIEW 这样的可视化语言。与这些语言不同,Unison 程序存储在一个尽可追加的(append-only)、内容寻址(content-addressed)数据库中。代码仍然以文本形式显示给用户以进行编辑,可以使用他们自己选择的编辑器,但它只解析一次,然后就存储在数据库内部了。以这个阶乘函数的实现为例:
factorial : Nat -> Nat
factorial n = match n with
0 -> 1
_ -> n * factorial (n - 1)
此函数的哈希值为 #in3bl5u64l
(根据 Unison 的默认 base-32 哈希格式来生成),使用 Unison 的自定义结构化哈希函数。哈希值是基于代码的结构,而不是用于表达它的变量名或格式。在内部实现上来说,代码的 抽象语法树 (AST) 在 Unison 的数据库中以该哈希值存储。如果另一个人编写了相同的函数,但决定将其命名为 fac
,它将具有相同的哈希值。在编辑引用它的其他函数时,Unison 将显示用户为其定义的任何名称;因此,一个人可能看到 factorial
,而另一个人可能看到 =fac=。这样,Unison 名称就像 Git 标签一样:是一个对象的人类可读的名称,该对象主要由哈希标识。
通常,程序员使用与运行 CLI 接口的终端或运行图形界面的浏览器窗口同时并列展示的编辑器与 Unison 交互。编写新代码时,用户会在其编辑器中输入代码,就像其他任何语言一样。保存时,Unison 会通过文件系统监视器收到通知,读取这些代码,然后对代码中存在的问题给出报告,没有问题就提供可以将其更新到数据库中的选项。编辑现有函数时,Unison 会将存储的定义采用 pretty-print 的方式输出到用户的编辑器中,并监视更改。这产生了有趣的副作用,即不再需要将代码的格式化动作作为单独的步骤——在程序员准备读取或编辑代码时,代码始终会进行格式化。总的来说,这种方法最终感觉更像是与编译器的协作,而不像传统的语言所做的那样。它会询问定义、建议要做的改动、指出问题和失败的测试等。以下是我将上述定义添加到代码中的样子:
I found and typechecked these definitions in /tmp/scratch.u. If you do
an `add` or `update`, here's how your codebase would change:
⍟ These new definitions are ok to `add`:
factorial : Nat -> Nat
Unison 的命名方法可能看起来像是一个有趣的奇闻趣事,但它有一些实际的影响。首先,重命名函数、变量或类型永远不会破坏任何东西。即使发生名称冲突也不会造成问题——Unison 通过哈希跟踪底层代码,因此两个都名为 foo
的项目可能会显示给用户为 foo#hash1
和 foo#hash2
,但程序仍然可以编译并运行,没有任何问题。另一个结果是能够无缝使用同一库的不同版本——同一函数的不同版本具有不同的哈希值,因此可以像具有相同名称的不同函数一样对待它们。这也意味着函数的哈希值不仅编码了它的代码,还编码了它的确切依赖项,这使得在计算机之间共享代码变得更加简单。
声称 Unison 代码是不可变的,就会引发一个问题:一旦函数被编写,它实际上如何更新?由于 Unison 代码存储在数据库中,因此该语言始终准确地知道哪些代码引用了特定函数。如果对函数的编辑没有改变其类型特征,那么该语言可以自动生成一个依赖于已更改函数的每个函数的新版本。旧版本不会被删除,但任何函数的名称都会更新以指向新版本。这样以来就可以编写行为测试(behavior tests),这些测试通过引用函数的旧版本来对两个实现进行比较。
如果对函数的更改没有保留其类型,Unison 会使用相同的知识为程序员生成一个“待办事项”列表,它会跟踪该列表,并随着冲突的解决而自动删除其中的条目。由于旧代码仍然存在于数据库中,因此代码永远不会因更改而“损坏”。旧版本仍然存在,并且可以运行、构建、检查等等,同时程序员正在处理新版本。新版本完成后,程序员可以立即切换到该版本。
功能
Unison 独特的命名方法可能处理依赖项,但正如容器的广泛使用所示,让程序在许多计算机上运行不仅仅是确保将依赖项与程序捆绑在一起。代码不仅依赖于库函数或类型,还依赖于程序外部的计算机状态。Unison 无法完全解决这个问题,但它确实有一个解决方案来帮助管理依赖于与外部世界交互的代码的复杂性:能力(abilities)。
能力是一种 效果系统(effect system)——一种在类型系统中跟踪指定代码片段需要什么才能正确运行的方法。最通用的能力称为 IO
,它代表执行任意 I/O 的能力,包括读取和写入文件、打开网络连接或读取有关计算机状态的信息。程序员可以在他们的程序中为每个函数都要求 IO
能力,但更常用的方法是考虑程序的每个部分需要能够做哪些具体的事情,然后声明更小、更受限制的能力。具有自定义能力的程序可以通过提供一个“处理程序(handler)”函数来运行,该函数描述如何实现能力,通常是根据另一个能力。例如,程序员可以提供一个 ReadEnvironment
能力,该能力允许程序获取环境变量的值。在正常使用中,处理程序会将其转换为 IO
能力,但一个能力可以有多个处理程序,因此测试套件可以使用一个提供预定义测试值的处理程序。
由于能力由类型系统跟踪,因此函数无法使用它未声明的能力。这意味着程序员可以通过查看类型签名来获取代码期望使用的每个与外部世界的接口的列表,并通过指定不同的处理程序来模拟它们以进行测试。总的来说,能力可以使编写可测试的分布式程序变得更加简单,因为一切都用一种灵活的语言描述。类型系统的保证也意味着理论上可以运行不受信任的代码,并确保它只访问程序员赋予它的能力。实际上,Unison 仍然处于开发阶段,安全性保证中可能存在一些 潜在的漏洞。
资金
Unison Computing 的创始人——Paul Chiusano、Rúnar Bjarnason 和 Arya Irani——对 Unison 的安全特性有足够的信心,认为其足以成为云计算产品的基础。Unison Cloud 是一个平台,允许运行使用自定义 Cloud
能力的 Unison 程序,每月支付费用,在托管的硬件上运行。这些资金流向了 Unison Computing,这是一家 公益公司,它雇佣了 Unison 的核心开发人员,以继续开发该语言。但是,该项目确实接受外部贡献,并且该语言本身将 保持开源。
Cloud
能力具有将任意 Unison 值存储到类型化数据库、处理 HTTP(S) 请求、部署新服务以及云中运行的程序所需的操作的功能。Unison Cloud 库也提供了 模拟处理程序(mock handlers),这些处理程序可以测试部署多个服务、运行健康检查和集成测试以及最终关闭本地部署的整个过程。
缺点
不幸的是,Unison 独特的设计也存在一些问题。首先,现代程序通常不是用一种语言编写的,但只有当整个程序都用 Unison 编写时,Unison 的最大优势才能发挥出来。Unison 甚至没有稳定的外部函数接口 (FFI),这种接口通常是用于包装用其他语言编写的库。因此,现有的 Unison 程序需要重新实现其他语言中已经存在的大量功能。
Unison Share 是软件包注册表、代码库和代码浏览器之间的交叉产物。由于 Unison 代码不是存储为文本文件,而是存储为数据库,因此社区实际上无法重用现有工具。像 Unison Share 这样的工具必须从头开始编写。支持将代码推送到 Git 存储库,但由于它不是人类可读的,因此在不使用本地 Unison 工具或托管 Unison Share 实例的情况下,实际上无法查看它。社区正在积极鼓励人们开发并在那里发布新库,但要赶上其他语言还有很长的路要走。尽管如此,Unison 在依赖项管理方面的想法使得使用现有的库变得非常容易——只需将它们拉入您的本地数据库并开始调用函数,无需担心依赖项冲突或从哪里获取代码。
这种方法确实提出了一个问题,即如何处理库的升级。上面描述的在函数更改时更新依赖代码的过程依赖于所有受影响的代码都可用于本地开发。这个问题有三个部分的回答:小型库、能力和补丁。由于 Unison 使得依赖库变得很容易,因此许多现有的库都很小;将一些小功能分解成一个单独的库比在其他语言中更容易。较小的库需要较少的更新,甚至可能完全完全不再需要更新。较大的库可以将其接口表示为一种能力。这使得升级到库的较新版本变得像改成新的处理程序(handler)那么简单。最后,对于上述两种方法都不适用的情况,Unison 会生成一种特殊类型的称为补丁的值——实际上,它是库开发人员在开发新版本时所做更改的记录,包括哪些新函数是由编辑旧函数产生的映射。Unison 使用该信息来执行与本地开发期间相同的升级。
Unison 正在积极开发中,尚未达到 1.0 版本。因此,除了抛弃熟悉的基于文本的工作流程之外,它还面临着任何语言都必须面临的正常挑战:性能问题、运行时中偶尔的错误、不稳定的接口等等。尽管如此,文档 已经相当全面,并且该项目有一项政策,即在升级时不会破坏现有程序。实际上,标准库是使用与其他库相同的过程进行管理的,因此程序很可能在内部使用标准库的不同版本而不会发生冲突。
Unison 尚未广泛打包,但可以从项目的 发布页面 下载。运行 ucm
(Unison 代码库管理器)将在 ~/.unison
中为用户代码设置一个数据库,并提供有关在 Unison 中启动项目的快速入门指南。
Unison 是否能克服成为一种广泛使用、高效语言的障碍,还有待观察。但是,即使它没有做到,它至少说明了软件开发的不同方法是可能的——一种将与计算机的协作直接构建到语言本身的方法,并提供对许多基于文本的编程语言的替代方案。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~