- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
本指南围绕 Unison 代码共享机制展开,聚焦"分享代码"时所需的底层数据模型——Causal因果链、命名空间(Namespace)与NameTree表示、以及远程引用gh:<user>/<repo>[/<path>][?ref=<treeish>]的解析方案。读完本文,你将理解 Unison 如何判断"某次变更是否发生在另一次之后"、如何在本地与远程代码树之间建立链接,以及如何在真实仓库源码中定位这些设计的实现。
说明:本文对应仓库中的 docs/sharing-code.markdown 设计笔记,并以当前仓库的源码实现为辅证。该文档是早期设计草稿,部分数据结构(如
RemoteStatus、GithubRef)在后续实现中有所演进,文中会同步给出对应源码位置。
代码共享的核心:Causal数据结构
共享代码首先要回答一个基础问题:两个代码快照之间,谁在谁之后?Unison 用Causal结构来表达这种"因果先后"关系。
设计文档中给出的抽象定义如下:
data Causal m e = One { currentHash :: Hash, head :: e } | Cons { currentHash :: Hash, head :: e, tail :: m (Causal e) } -- The merge operation `<>` flattens and normalizes for order | Merge { currentHash :: Hash, head :: e, tails :: Map Hash (m (Causal e)) }Causal有三种形态:
One:单个节点,代表一条因果链的起点,只包含当前哈希currentHash与头部值head;Cons:线性后继,head是最新值,tail指向历史;Merge:合并节点,tails :: Map Hash (m (Causal e))指向多个父节点。文档特别强调,合并操作<>会"展平并规范化顺序"(flattens and normalizes for order),保证合并结果的确定性。
在e上要求一个交换半群(CommutativeSemigroup)约束,从而可以确定性地把多个分支的头部内容合并成一个新head。
源码中的Causal实现
设计文档中的Causal在仓库中有两个可对照的实现:
抽象代数化定义: parser-typechecker/src/Unison/Codebase/Causal/Type.hs 中的
Causal m e,同样由UnsafeOne、UnsafeCons、UnsafeMerge三种构造器组成。其模块注释用代数公理完整刻画了Causal上的五个操作(见同文件 L24-L42):before : Causal a -> Causal a -> m Bool:定义Causal上的偏序;head:返回因果链中的"最新"值;one:构造单节点;cons:在线性链上追加新头部;merge:交换(但不结合)的合并操作,满足before c1 (merge c1 c2)与before c2 (merge c1 c2);sequence c1 c2 = cons (head c2) (merge c1 c2),是"合并后再取另一侧头部"的复合操作。
同文件 L88-L89 用
before a b = (== Just a) <$> lca a b实现了"先后"判断:a在b之前,当且仅当a是b的最低公共祖先(LCA)。lca的搜索采用双向广度优先(见 lca'),交替扩展左右两侧的predecessors,直到某一侧命中另一侧已访问的currentHash。惰性版实现: codebase2/codebase/U/Codebase/Causal.hs 中的
Causal m hc he pe e进一步泛化:parents :: Map hc (m (Causal m hc he pe pe))以父哈希为键、以m (...)包一层惰性求值,只有需要时才加载父节点,value :: m e同样惰性。Eq实例按causalHash比较(L22-L23),并提供emap对值与父值做映射、hoist做单子变换。
这两个实现与设计文档一脉相承:currentHash对应索引哈希,parents/tails对应可枚举的先驱节点。Unison 的命名空间(Namespace)正是包在Causal里,才使得"共享一个分支、并知道它相对于其他分支的位置"成为可能。
命名空间与NameSegment/Path
共享的是命名空间,因此文档紧接着定义了一级名称与路径的表示:
-- just one level of name, like Foo or Bar, but not Foo.Bar newtype NameSegment = NameSegment { toText :: Text } newtype Path = Path { toList :: [NameSegment] } data Namespace m = Namespace { terms :: Relation NameSegment Referent , types :: Relation NameSegment Reference , children :: Relation NameSegment (Codetree m) } data Codetree m = Codetree (Causal m Namespace)要点:
NameSegment是单个层级的名称(如Foo、Bar),不含点号或斜杠;Path是若干NameSegment组成的序列,即Foo.Bar这类多级路径;Namespace三个字段分别记录:该层级下的项(terms)(NameSegment→Referent)、类型(types)(NameSegment→Reference)以及子命名空间(children)(NameSegment→Codetree m)。三者都是Relation,即多对多关系的集合式表示;Codetree m = Codetree (Causal m Namespace)明确点出:每一个子命名空间自身也是一棵由Causal驱动的代码树,这使任意子路径都可以独立共享、独立判断因果先后。
当前仓库中,NameSegment的实现位于 codebase2/core/Unison/NameSegment.hs,内部定义在Unison.NameSegment.Internal(引用处)。该文件还定义了一批哨兵(sentinel)名称段:docSegment("doc")、libSegment("lib")、baseSegment("base")、builtinSegment("builtin")等(L31-L80)。这些是 Unison 保留的固定命名层级(如lib下存放外部库、builtin下存放内建定义),共享命名空间时需特别注意避开。
新版代码库中,Namespace的演进版出现在 codebase2/codebase/U/Codebase/Branch/Type.hs:Branch m用Map NameSegment (Map Referent (m MdValues))表示 terms、Map NameSegment (Map Reference (m MdValues))表示 types,CausalBranch m = Causal m CausalHash BranchHash (Branch m) (Branch m)(L34)则是对"Causal包裹命名空间"这一设计的直接落地——相比文档草稿,它把关系集合改成了映射,并为每个名称下的引用挂上惰性的元数据集合。
命名空间与编辑操作的关系
文档提出了三个"重要观点",是理解整篇设计的钥匙:
- 命名空间只是你偏好的解析(以及一定程度上的渲染)代码的方式。命名空间结构本身是可变的、随偏好而异的,不应与底层的、按哈希寻址的定义存储混为一谈;
- 编辑(Edits)只是编辑辅助命令(如
todo、propagate)的状态。换句话说,Edits不是代码共享的必需品,而是编辑器辅助功能的内部状态; - 应该考虑让代码库对这一数据的表示保持模块化,因为二者确实可以分离——即使未来出现文档/编辑器支持其他功能所需的意外状态与偏好,这些数据依然有意义。
对应地,文档给出了编辑状态的数据结构:
newtype EditMap = EditMap { toMap :: Map GUID (Causal Edits) } data Edits = Edits { terms :: Relation Reference TermEdit , types :: Relation Reference TypeEdit } -- maps local paths to remote paths data RemoteStatus = Map Path RemoteSpec值得注意:EditMap中的每个编辑集(由GUID标识)本身也是一个Causal——编辑历史同样需要判断先后、同样可以被合并。Edits记录了引用(Reference)被如何编辑:terms映射到TermEdit,types映射到TypeEdit。
当前仓库中这两类编辑的定义为:
- codebase2/codebase/U/Codebase/TermEdit.hs:
data TermEdit = Replace Referent Typing | Deprecate; - codebase2/codebase/U/Codebase/TypeEdit.hs:
data TypeEdit = Replace Reference | Deprecate。
也就是说,一个编辑要么把某引用替换为另一个引用(替换项需要携带新的类型信息),要么将其弃用(Deprecate)。EditMap的现代形态见 codebase2/codebase/U/Codebase/Branch/Type.hs 的Patch记录:termEdits :: Map Referent (Set TermEdit)与typeEdits :: Map Reference (Set TypeEdit)。
而RemoteStatus(把本地路径映射到远程规格RemoteSpec)在设计文档中只是占位定义,后续设计在 docs/branchless.md 中演进为更完整的RemotePath与Link机制(详见下文"远程引用"一节)。
名称的分隔符问题:/还是.
文档提出一个待决问题:是否要在名字中区分/路径分隔符和.分隔符?讨论围绕"类型A及其构造子A.A应处于什么层级"展开:
- 一方面,你通常不需要把类型
A与其构造子A.A分离——你无法在不导出上一级命名空间中的类型的情况下单独导出构造子; - 另一方面,类型
A也许应该自然地组织成A/A,其构造子同样位于A/A。这让人想起 Haskell 中"每种类型一个独立模块"的做法,但在 Unison 中重新组织会更加容易,文档给出了命令行示例:
/mycode> mv ClassA* ClassA/ /mycode> mv ClassB* ClassB/ /mycode> cd ClassA /mycode/ClassA> ls .这段会话演示了:在/mycode下把ClassA*(类型、构造子等所有相关名称)整体移动进ClassA/子目录,ClassB同理,然后进入ClassA查看。mv命令在后续的 docs/branchless.md 中有进一步说明:它可以同时针对 Terms、Types、Directories 或三者全部,并用哈希限定名来区分歧义。
NameTree表示
为承载"命名空间树"这一概念,文档给出了两种NameTree表示方案:
<empty> /A (type) /A (term) /A/A (ctor)data NameTree a = Causal (Relation Name (NameTree a))或带叶子与共享点的变体:
data NameTree a = Leaf a | Branch (Relation Name (NameTree a)) | SharePoint (Causal (NameTree a))第一种方案把整棵树包进一个Causal,树的每个分支节点都是Relation Name (NameTree a);第二种方案则显式区分三种节点:
Leaf a:叶子,承载值;Branch (Relation Name (NameTree a)):普通分支,挂载子树;SharePoint (Causal (NameTree a)):共享点——此处是一棵独立的Causal子树,意味着该子命名空间可以被单独共享/克隆。
SharePoint正是"按目录共享"的表示基础:只有挂载了SharePoint的子路径,才具备独立的因果历史,才适合作为共享的边界。仓库中SharePoint思想的后续体现在docs/branchless.md的Link类型:data Link m = LocalLink (Branch' m) | RemoteLink RemotePath(docs/branchless.md),本地链接指向本地分支,远程链接指向远程RemotePath,避免在本地重复分发外部库。
远程引用:gh:<user>/<repo>[/<path>][?ref=<treeish>]
共享的关键在于如何在本地命名空间中引用远程命名空间。文档定义了远程路径与远程引用的数据结构:
data RemotePath = RemotePath RemoteRef Path data RemoteRef = GithubRef { username :: Text, repo :: Text, treeish :: Text } -- | ... -- "gh:<user>/<repo>[/<path>][?ref=<treeish>] -- treeish defaults to repo's `default_branch` -- "gh:aryairani/unison/libs?ref=topic/370" becomes -- RemotePath (GithubRef "aryairani" "unison" "topic/370") (Path ["libs"])解析规则:
- URI 语法:
gh:<user>/<repo>[/<path>][?ref=<treeish>]; treeish(分支名、tag 或 commit)缺省时使用仓库的default_branch;- 例:
gh:aryairani/unison/libs?ref=topic/370解析为RemotePath (GithubRef "aryairani" "unison" "topic/370") (Path ["libs"])——即"aryairani/unison 仓库中topic/370分支下的libs目录"。
这一设计的后续版本在 docs/branchless.md 中展开为:
data BranchPath = BranchPath RepoRef Path data RepoRef = Local | GithubRef { username :: Text, repo :: Text, treeish :: Text }其中Local分支路径形如BranchPath Local (Path ["libs","community","DL"])(对应本地路径/libs/community/DL),远程则形如BranchPath (GithubRef "aryairani" "unison" "topic/370") (Path ["libs"])(对应gh:aryairani/unison/libs?ref=topic/370)。
treeish可能包含斜杠(如topic/370),这使解析变得棘手。文档给出的应对策略依赖 Git 的一个性质:如果存在分支a/b,就不可能再创建分支a或a/b/c。因此可以:
- 先从 GitHub API 拉取分支列表(
https://api.github.com/repos/<user>/<repo>/branches); - 用每个分支名加
/去匹配treeish-prefixed-path的前缀; - 命中分支前缀后,剩余后缀即命名空间内的路径。
不过文档随即自我修正:GitHub 的网页 UI 根本不会展示 Unison 的路径结构,所以这个"从 GitHub URL 反推路径"的思路意义有限——最终建议是坚持使用自定义的gh:username/repo[:treeish][/path]URI 方案,并让 Unison 的 JavaScript 查看器生成带查询参数/片段的 URL(如?branch=<hash>&path=<path>)用于分享。
附:GitHub API 资源形态笔记
文档末尾记录了 GitHub 目录与文件在 API 中的资源形态,可作为实现远程引用的参考(以下 URL 原属unisonweb/unison仓库的示例,仅用于说明 API 返回的字段结构):
目录(如unison-src/demo,ref=master):
url: https://api.github.com/repos/unisonweb/unison/contents/unison-src/demo?ref=master html_url: https://github.com/unisonweb/unison/tree/master/unison-src/demo git_url: https://api.github.com/repos/unisonweb/unison/git/trees/f8d91c6cc2ee1bc8f2bfc759e328a851d0df3b95文件(如unison-src/Base.u,ref=master):
url: https://api.github.com/repos/unisonweb/unison/contents/unison-src/Base.u?ref=master html_url: https://github.com/unisonweb/unison/blob/master/unison-src/Base.u git_url: https://api.github.com/repos/unisonweb/unison/git/blobs/e617fbad4e32d25380f536179f558f9213cd4bad download_url: https://raw.githubusercontent.com/unisonweb/unison/master/unison-src/Base.u要点:GitHub 把网页上可见的 URL 称为html_url,可用任意 treeish(分支、tag、commit)参数化引用文件或目录;但 GitHub 自身并不知道 Unison 命名空间的内部路径,因此远程共享的主协议仍是gh:URI。
远程引用方案的整体小结
综合设计文档与后续 docs/branchless.md 的演进,Unison 代码共享的完整链路可以归纳为:
- 命名空间是
Causal包裹的数据:每个命名空间(含子命名空间)都是一条可判断先后的因果链,One/Cons/Merge三种形态覆盖单点、线性和合并三种演化方式; - 编辑与命名空间解耦:
EditMap/Edits只是todo、propagate等编辑辅助命令的状态,TermEdit/TypeEdit仅表达"替换"与"弃用"两种动作,未来编辑器的新偏好也不会污染共享数据模型; - 本地/远程引用分离:本地子路径用
Local或直接内联,外部依赖用RemoteLink RemotePath(GithubRef)引用,避免重复分发外部库;发布时通过"传递性发布算法"把本地依赖镜像进./_Libs(若冲突则_Libs1,依此类推),远程依赖则携带指向远程仓库的链接(docs/branchless.md); - URI 语法收敛:以
gh:<user>/<repo>[/<path>][?ref=<treeish>]为主要远程引用语法,treeish缺省为default_branch,借助"Git 分支名不互为前缀"的性质消除解析歧义。
相关源码与文档导航
- 设计文档本体:docs/sharing-code.markdown
- 分支/命名空间设计的姊妹篇:docs/branchless.md(含
Causal、Namespace、Edits、远程同步与 GitHub 解析的展开讨论) Causal代数化实现:parser-typechecker/src/Unison/Codebase/Causal/Type.hs(One/Cons/Merge模式、before、lca)Causal惰性泛化实现:codebase2/codebase/U/Codebase/Causal.hs- 命名空间/分支的现代表示:codebase2/codebase/U/Codebase/Branch/Type.hs
NameSegment及哨兵名称段:codebase2/core/Unison/NameSegment.hs- 编辑动作定义:codebase2/codebase/U/Codebase/TermEdit.hs、codebase2/codebase/U/Codebase/TypeEdit.hs
- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
相关推荐
Unison 库代码复用与发布设计:从分支合并到 gh: 远程引用的演进
Unison 库代码复用与发布设计:从分支合并到 gh: 远程引用的演进 本文以仓库内 docs/publishing library1.md https://
编程语言编译器语言运行时开发工具Unison 命令行 Tab 补全:从本地命名空间到远程 Share 的完整机制解析
Unison 命令行 Tab 补全:从本地命名空间到远程 Share 的完整机制解析 导读 本篇指南聚焦 Unison 代码管理工具(UCM)中 debug.t
编程语言编译器语言运行时开发工具如何 10 分钟上手 Tailwind CSS:零基础快速入门完整教程
如何 10 分钟上手 Tailwind CSS:零基础快速入门完整教程 本文将带你认识 Tailwind CSS —— 一个 Utility First(工具类
前端前端构建
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考