让GHC升级更简单
摘要
GHC团队概述了使GHC升级更简单的进展,重点关注Big Stability Goal和Base Package Goal,以将基础包从编译器发布中解耦。
<p><a href="https://lobste.rs/s/njidax/making_ghc_upgrades_easy">评论</a></p>
查看缓存全文
缓存时间: 2026/06/17 11:43
# 让 GHC 升级变得简单 | Haskell 编程语言博客
来源:https://blog.haskell.org/making-ghc-upgrades-easy/
## 让 GHC 升级变得简单 (https://blog.haskell.org/making-ghc-upgrades-easy/)
Simon Peyton Jones (https://blog.haskell.org/authors/simon-peyton-jones/) 2026年6月12日 \[GHC (https://blog.haskell.org/categories/ghc/)\] \#hackage (https://blog.haskell.org/tags/hackage/) \#cabal (https://blog.haskell.org/tags/cabal/)
## 本文内容
当闪亮的新版 GHC 发布时,升级应该很容易。毕竟,新版编译器应该比旧版更强大!
但实际上几乎并非如此。升级路径如此困难,以至于许多公司仍在使用多年前的 GHC 版本;升级对他们来说工作量太大。这在很多方面都很糟糕:
- 它剥夺了这些用户访问新版 GHC 中体现的 bug 修复和前沿工作的权利。
- 它极大地抑制了这些用户投入资源*贡献*给 GHC,因为他们所做的工作会落在旧版本上,而不会传播到 HEAD。
- 它给 GHC 的实现者增加了维护负担,需要支持更多旧版本。
GHC 团队一直在努力解决这个问题,并取得了很大进展。本文总结了我们做了什么、还有什么需要做,并邀请您提供帮助。
## 1. 目标
我们有两个大目标。最重要的是这一个:
### (稳定性目标)大稳定性目标。
假设一个包 P 可以成功使用 GHC 10.0 编译。当 GHC 10.2 发布时,应该可以使用 GHC 10.2 编译包 P 及其所有依赖项,*无需修改*。
不可能 100.0% 承诺实现(稳定性目标):请参见下面的第 4.1 节 (https://blog.haskell.org/making-ghc-upgrades-easy/#4-1-caveats)。但我们可以非常接近。
还有一个补充目标,涉及 `base` 包。`base` 包为每个 Haskell 程序提供核心功能,包括 Haskell 报告指定的所有模块,尤其是 `Prelude` 模块。几乎每个存在的 Haskell 包都直接或间接依赖于 `base`。(因此得名。)
### (基础包目标)基础包目标。
`base` 包应该像其他任何包一样:
- 拥有自己的仓库
- 拥有自己的维护者
- 拥有自己的变更日志,独立于 GHC 的变更日志。
- 按独立于 GHC 的时间表发布
- 可以独立于 GHC 进行重构。
- 你使用的 `base` 版本显然必须与你使用的 GHC 版本兼容;但你可以使用任何具有该属性的 `base` 版本。
我们现在非常接近实现这些目标。本文解释了为什么这比看起来更难,我们最近做了什么,以及“非常接近”意味着什么。
这两个目标看起来是独立的,但事实上克服一组障碍将同时解锁这两个目标,这就是我在这里把它们放在一起讨论的原因。第 2 节 (https://blog.haskell.org/making-ghc-upgrades-easy/#2-background-the-problem) 描述了问题。
## 2. 背景:问题
过去,每个版本的 GHC 都附带一个新版本的 `base` 包。例如:
- GHC 9.8 附带 base-4.19.0
- GHC 9.10 附带 base-4.20.2
- GHC 9.12 附带 base-4.21.1
- GHC 9.14 附带 base-4.22.0
**此外,每个版本的 GHC 都不可分割地与且仅与一个版本的 `base` 绑定。** 例如,每个使用(比如)GHC 9.10 编译的程序都必须针对 `base-4.20.2` 编译。没有其他版本的 `base` 可以替代。
### 2.1 为什么紧密耦合是个问题
这种紧密耦合对早期版本 GHC 的实现者来说很方便,那是在我们考虑稳定性之前,甚至在 Cabal 和 Hackage 出现之前。但事后看来,这种紧密耦合是非常不可取的:
- 假设包 P 使用 GHC 9.10 编译,针对 `base-4.20.2`。
- 要使用 GHC 9.12 编译 P,我必须使用 `base-4.21.1`,因为 GHC 9.12 坚持这样做。
- 所以至少,我必须更新 P 对 `base` 的上限。
- `base` 版本号改变的原因是核心库委员会批准的变更,可能是添加新函数,或删除某些函数。这些变更可能迫使 P 发生不仅仅是依赖界限变更的改动。
- 即使 P 对新 `base` 没问题,也许 P 依赖于 Q;因此直到 Q 适应了新 `base`,P 才能编译。但 Q 的维护者可能忙于其他事务,所以适应可能需要一段时间。
- 实际上,P 可能直接或间接依赖于几十个包,所有这些包都必须适应新的 `base`(可能只需更新版本界限,但也可能更多)。
这非常非常糟糕。可能需要几个月时间才能完成一波版本界限更新并波及整个 Hackage。这直接违背了(稳定性目标)。
### 2.2 “已知实体”问题
但是,*为什么*比如 GHC 9.10 坚持使用 `base-4.20.2` 而不允许其他版本?
主要原因是 GHC 需要“知道”定义在 `base` 中的数百个函数、类型和类。例如:在为新数据类型生成 `deriving(Show)` 的代码时,生成的代码需要引用 `base` 中定义的辅助函数。这里的“知道”是指 GHC 需要知道 `Show` 类(以及许多其他辅助函数)定义在哪个精确的模块中。
还有很多很多其他例子:列表推导式的脱糖、箭头表示法或记录访问。这些函数、类型和类统称为“**已知实体**”。
`base` 和 GHC 之间的这种紧密耦合直接违背了(基础包目标)。
## 3. 宏伟计划及进展
我们在实现(稳定性目标)和(基础包目标)方面取得了很大进展。本节列出我们已经采取或提议采取的步骤。
这是一个多年项目,涉及许多人的贡献;时间线和致谢见第 6 节 (https://blog.haskell.org/making-ghc-upgrades-easy/#6-credits)。
### 3.1 拆分 `base` 和 `ghc-internal`
第一步是将旧的 `base` 库拆分为两个库:`ghc-internal` 和 `base`。
- `ghc-internal` 实际上应该被视为 GHC 的一部分,只是恰好在库代码中实现而不是在编译器本身中。它的 API 不稳定,每个版本的 GHC 都紧密耦合到一个新版本的 `ghc-internal`。GHC 和 `ghc-internal` 库应被视为一个软件实体,位于一个仓库中,并且有一个版本号。
- `base` 依赖于 `ghc-internal`。然而,与后者不同,`base` 有一个非常稳定的 API,由核心库委员会精心管理。`base` 的一种理解方式是,它是一个垫片层,将 `ghc-internal` 的变化隐藏在稳定的 `base` API 之后。
这种架构原则上具有主要优势:
- 新版本的 GHC 总是附带新版本的 `ghc-internal`,但现在它可以附带相同版本的 `base`。这解决了(稳定性目标)。所谓“相同版本”,是指其 API 不变;当然它的*实现*可能会有很大变化,以适应 `ghc-internal` 的变化。这种 API 稳定性反映在 PVP 版本 `base-A.B.C.D` 中。新版(API 未变)应仅在其最后的组件 `C` 或 `D` 上有所区别。通常,包的依赖关系如 `base-4.22`,允许升级到 `4.22.0.1` 甚至 `4.22.1` *而无需修改*;而这种无需修改即可编译正是(稳定性目标)。(旁注:Haskell PVP (https://pvp.haskell.org/) 在修复 bug 或进行内部重构时对版本更新较为沉默。)
- `base` 库可以独立维护,并且可以像其他任何包一样独立于 GHC 发布。这解决了(基础包目标)。例如,如果核心库委员会决定向 `base` 添加一个函数 `wombat`,维护者可以添加该函数并发布新版本的 `base`,就像其他任何包一样。此外,像其他任何包一样,`base` 的维护者可以使其与多个版本的 GHC 兼容,这样用户就可以不受编译器版本限制地升级或选择其版本。也就是说,`base` 变得“可重新安装”。
这种分离在 GHC 9.14 中实现。执行起来比看起来更棘手;首先必须分离 `ghc-internal` 和 `base`,然后让 `base` 变得可重新安装。(时间线见下面的第 6 节 (https://blog.haskell.org/making-ghc-upgrades-easy/#6-credits)。)
即使在此之后,所有“已知实体”(见第 2.2 节 (https://blog.haskell.org/making-ghc-upgrades-easy/#2-2-the-problem-of-known-entities))仍必须定义在 `ghc-internal` 中。实际上,这导致大量库代码固定在 `ghc-internal` 中,并且意味着 `base` 主要只是一个垫片层。(仍然有用!但没有太多自身功能)。下一步见第 3.3 节 (https://blog.haskell.org/making-ghc-upgrades-easy/#3-3-known-entities)。
### 3.2 Template Haskell
Template Haskell 允许你创建一个源 Haskell AST(抽象语法树),并对其进行模式匹配。由于每个版本的 GHC 都会对其 Haskell AST 进行更改,任何在 Template Haskell AST 上进行模式匹配的包都无法与新版 GHC 编译。
这直接威胁到(稳定性目标)。更具体地说:
- 使用 Template Haskell 的模块依赖于一个名为 `template-haskell` 的库。
- `template-haskell` 的 API 包含一个 Haskell AST 的数据类型,该类型必然在每个 GHC 版本中发生变化,迫使 `template-haskell` 的版本发生重大更新。
- 许多许多库都传递性地依赖于某个使用 Template Haskell 的库 L。
- 因此,这些库直到 L 的作者至少更新了其对 `template-haskell` 依赖的版本界限后,才能与新版 GHC 一起工作。
这导致每当新版本 GHC 发布时,就会出现大量破坏。而且大部分破坏是不必要的!大多数使用只涉及引用和拼接(这些都是完全可移植的),而不是直接使用 AST(这是不可移植的)。有关不同类别 Template Haskell 用法及其不同稳定性属性的更多细节,请参见 Teo 的博客文章 (https://informal.codes/posts/stabilising-th/),或他们在 2025 年 Haskell 生态系统研讨会上的演讲 (https://informal.codes/talks/hew25/)。
因此,Teo 一直在忙于拆分以前单一的 `template-haskell` 包(其 API 必然随每个 GHC 版本变化)为几个包:
- `template-haskell-lift`
- `template-haskell-quasiquoter`
这些新包具有非常稳定的 API。仍然有一个 `template-haskell` 包暴露了 TH AST 数据类型,这必然是不稳定的。但很少有客户端需要依赖它。(旁注:你可能会认为它应该叫 `template-haskell-internal`,你是对的;但这会带来破坏性变化,我们暂时不会这样做。)
这些新包已经存在于 GHC 9.14 中,所有引导库现在都依赖它们而不是 `template-haskell`。(旁注:虽然这些更改已落地到相关的引导库仓库中,但在撰写本文时尚未全部发布。)到 GHC 10.2 时,`ghc` 包本身将不再传递性地依赖于 `template-haskell`。
然而,要获得好处,使用 Template Haskell 的库作者必须更新他们的包以依赖稳定的 API,即:
- TH 拼接和引用
- 稳定库 `template-haskell-lift` 和 `template-haskell-quasiquoter`
并移除对 `template-haskell` 的依赖。
更多背景信息:
- Stabilising Template Haskell (https://informal.codes/posts/stabilising-th) (2024 年博客文章)
- Announcing template-haskell-lift and template-haskell-quasiquoter (https://informal.codes/posts/ann-th-lift-and-quasi/) (2025 年博客文章)
- Template Haskell: a base study in (in)-stability (https://informal.codes/talks/hew25/) ,2025 年 Haskell 生态系统研讨会
- The abstract Q proposal (https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0700-abstract-q.rst) (将落地于 GHC 10.2,不是面向用户的,但开辟了新的机会)
### 3.3 已知实体
直到并包括 GHC 10.0,对于每个“已知实体”E(函数、类型或类;见第 2.2 节 (https://blog.haskell.org/making-ghc-upgrades-easy/#2-2-the-problem-of-known-entities)),GHC 坚持要求:
- E 定义在 `ghc-internal` 中。
- GHC 知道(硬编码在 GHC 源代码中)E 所在的模块。
我们最近的工作(将在 10.2 中实现)意味着,有史以来第一次,这些“已知实体”不再需要定义在已知模块中。相反,E 可以定义在 `base` 或 `ghc-internal` 的任何模块中。
此外,`base` 中的更改可以将 E 从一个模块移动到另一个模块,而无需更改 GHC。例如,使用 GHC 10.2,你可以针对两个版本(它们在同一名称的模块中定义 E)的 `base` 编译一个模块 M,而无需更改 GHC 本身。那么 GHC 如何找到 E?它在 `base` 模块 `GHC.Essentials` 的导出列表中查找。`base` 的作者只需确保 `GHC.Essentials` *导出*所有已知实体即可;但它们不必*定义*在 `GHC.Essentials` 中。
这种架构正确地支持了(基础包目标),因为它允许 `base` 的维护者自由地重构代码,*包括*将已知实体从一个模块移动到另一个模块。
更好的是,它还允许我们将代码从 `ghc-internal` 移动到 `base`。这很好,因为对 `ghc-internal` 中代码的 bug 修复只能与新 GHC 版本一起发布,而对 `base` 中代码的 bug 修复可以独立于编译器进行并发布。我们能够从 `ghc-internal` 中移除并放入 `base` 的代码越多,效果越好!
### 3.4 清理 `base` API
由于历史原因,`base` 导出了不少应被视为 GHC 内部的函数——它们被“祖父条款”留在了 `base` 中。例如,`base:GHC.Base` 导出 `mapFB`,这是一个仅在 GHC 内部用于列表融合实现的函数。另一个更基础的例子:`base:GHC.IO` 暴露了 `IO` 的*表示*,而不仅仅是 `IO` 的 API。这里有一份所有 base 模块的列表,并附有稳定性和状态说明 (https://docs.google.com/spreadsheets/d/1WmyYLbJIMk9Q-vK4No5qvKIIdIZwhhFFlw6iVWd1xNQ/edit?gid=1315971213#gid=1315971213)。
尽管这些导出可能是历史性的且有些偶然,但包可能仍然依赖它们。这从各方面看都不好:
- 那些包作者可能会受到 GHC 内部有时不可避免的更改的影响。
- 它抑制了 GHC 的实现者更改应属于 GHC 内部的内容,即使这样做是合理的。
- 它迫使 `base` 进行主要版本更新,即使 GHC 只是对 GHC 内部函数的某个偏僻角落进行了更改。这种主要版本更新会迫使整个生态系统发生一系列更改,而其中 99.9% 的包既不关心也不了解这个 GHC 内部函数。
- 如果 `base` 暴露了 GHC 特定的函数,就会使得其他编译器(例如 MicroHs (https://hackage.haskell.org/package/MicroHs))难以或不可能支持 `base`。
显而易见的问题是:为什么不从 `base` 的 API 中移除这些偶然的导出?有两个原因:
- 它们可能暴露了真正有用的功能。**正确的做法是仔细设计一个稳定的 API,并在未来承诺维护它**。只要它们仍能支持这个 API,GHC 的内部可以改变。
- 无论设计或实用性如何,一些现有包可能依赖于这些函数。移除函数 `foo` 会有什么影响?CLC 会对提案做出回应,如果某人提出这样的提案,CLC 会要求进行这样的影响分析。
所以这里有一个任务,我们尚未
相似文章
Haskell Foundation 2026 年更新
Haskell Foundation 发布了其 2026 年的活动与计划更新。
@charliermarsh: 难以置信的是,借助 /goal,你可以在后台持续改进你的软件。让它更快…
Charlie Marsh 称赞 /goal 工具能够实现后台持续改进软件,使其更快、更小、更稳定。
@thsottiaux: 对于 Codex,我们一直在考虑保持稳定的发布节奏,并每周四进行一次较大的版本更新。……
一位开发者讨论了 GitHub Copilot Codebase 计划采用稳定的每周发布节奏,并在每周四进行较大更新的方案。
构建系统重构
Zig 构建系统已经重构,将配置器和制造器进程分离,支持缓存、发布模式编译,并且'zig build'命令速度提升高达90%。这一变化提高了性能,并允许构建系统在不减速的情况下增加功能。
Zig 构建速度正在提升
Zig 0.15 相比 0.14 在编译时性能有显著提升,构建脚本编译时间从约 7 秒降至约 1.7 秒,完整构建时间从 41 秒降至 32 秒,且仍使用 LLVM。本文重点介绍了自托管后端和增量编译方面的进展。