Mojo 语言演进实录:`let` 声明移除提案(remove-let-decls)的设计论证与仓库落地验证
本文基于 Mojo 语言提案《Simplifying Mojo🔥 - let's get rid of
let》(作者 Chris Lattner,2023 年 12 月,Status: Accepted)展开。该提案是 Mojo 早期语言设计中最具代表性的简化决策之一:彻底移除"具名不可变变量"let,仅保留var(与def中 Python 风格的隐式声明),并把不可变性收束到借用参数(borrowed arguments)与未来的不可变引用(immutable references)上。读完本文,你将完整理解该提案的动机、论点、分阶段落地计划,以及它在当前仓库发布记录(v0.24.1、v0.24.2、v0.24.4)中的真实执行结果,可直接用于理解 Mojo 现代语法(var/alias体系)的来龙去脉。
一、背景:Mojo 0.6 时代的 let/var 双关键字设计
Mojo 在开发早期沿用了 Swift 的先例,允许程序员用 let 与 var 两个关键字自行决定某个局部名字是否可修改。提案明确指出,Mojo 0.6 及更早版本中的这一设计存在一系列"特别令人意外且不讨喜"(particularly surprising and unfortunate)的方面,归纳起来有八条:
- 对 Python 程序员完全是陌生概念。具名不可变变量对 Python 程序员而言是全新概念,而 Swift 的实践经验表明,这往往成为 Swift 初学者需要学习的第一个概念(Constants and Variables)。但具名不可变变量并非核心编程概念,也不是达成 Mojo 目标所必需的。
let的命名引发大量早期争论。let、val、const等关键字在不同语言(如 C/C++、JavaScript 的const)中各有取舍,let的命名本身就是一个持续消耗社区精力的 bikeshed 话题。- 与编译期值
alias概念重叠。Mojo 还拥有编译期值(compile time value,alias),因此同一套语言里同时存在alias、let、var三个相近概念。多数场景下,其他语言(如 JavaScript)中用const表达的意图,在 Mojo 中用alias表达反而更贴切。 - 与 Python 的设计重心相悖。Swift 与 Rust 都鼓励不可变:Swift(以及当时的 Mojo)会对不必要的可变性发出警告,Rust 则让可变性更啰嗦(
let mut)。而 Python 压根没有这个概念——若 Mojo 将不可变设为默认,会显得怪异;若不做默认,又何必保留它。 - 没有可验证的性能收益。提案作者明确表示没有看到任何数据显示
let/var区分能带来安全或可读性收益。唯一的论点是看到let x = foo()时知道x永不被重新赋值,但这种收益很小。 - 引用语义类型下极度困惑。不可变性只作用于局部值本身,对引用语义类型(如当时的
Pointer,以及未来 Mojo 的所有类)会造成强烈误导。社区高频提问:"我明明在修改指针指向的目标,为什么警告我该把var指针改成let?" - 不允许
let作为结构体字段,规则不一致。Swift 为结构体字段初始化制定了非常复杂的规则,Mojo 并不想复刻。同时,默认字段值也没有好的定义方式,例如提案中的设想代码:
struct Thing:
# 当前并不支持,但可以设想:
let field = 42
def __init__(out self):
self.field = 17 # 是否不应允许覆盖 field?
- 与所有权/生命周期体系形成概念叠加。Mojo 拥有所有权(ownership)概念,未来还会有生命周期(lifetimes)与安全引用(包括可变与不可变引用)。不可变性以多种形态漂浮在语言中并不健康,而不可变借用与不可变引用恰恰是真正需要的。
提案作者还以 Swift 主要设计者之一的身份做了主观反思:Swift 为了提升安全而加入了不少相当挑剔的语言特性(例如要求所有可能抛错的值用 try 标记),且许多早期决策缺乏数据支撑。因此,应尽力让 Mojo 保持易学,并尽可能消除多余概念。
二、提案核心:彻底消除 let 声明
提案的核心主张非常直接:彻底消除"具名不可变值"这一概念。这并不会消灭 Mojo 中的不可变性,而是把它推入借用参数与不可变引用的世界中。该决策同时带来两大块收益:
简化概念模型:
- 消除 Python 程序员必须学习的一个陌生概念;
- 直接消除
let与alias之间的混淆; - 消除关键字命名之争(keyword bikeshedding)这一争论源泉;
- 消除工作簿(workbook)中"顶层值声明为
let却实际上可变"的困惑。
削减编译器复杂度:
- 错误信息不再需要小心翼翼地分辨该说
let还是var; - IR 表示不再需要为后续语义检查跟踪该信息;
- 无需再为支持
let而实现缺失的功能; - 无需再实现"检测未被修改的
var并警告改成let"这一整套逻辑; - 由于 ASAP 析构(ASAP destruction),
CheckLifetimes不得不额外增加复杂度去拒绝形如let x: Int; x = 1; use(x); x = 2; use(x)的代码——尽管第一个x = 1的生命周期天然结束、x在再次赋值前本就处于未初始化状态。提案认为这始终是一个设计瑕疵,且实际实现效果并不正确。
提案同时给出明确结论:该提案就目前所知不会对运行时性能产生任何影响——移除的是语言层面的语法与语义负担,而非底层执行能力。
补充:提案中提到的"未来的 lifetimes 特性"在当前仓库中已有对应设计文档 lifetimes-and-provenance.md,与"不可变性主要落在借用与引用层面"的路线一脉相承。
三、var 何去何从?
提案明确表示:如果移除 let,应原样保留 var。理由有二:
- 与传统 Python 行为不同,
var引入的是显式声明且词法作用域(lexically scoped)的值——Mojo 需要某种引入符(introducer),也确实需要作用域声明; var这个名字争议更小,因为它比let代表"具名常量"更清晰地表示"变量"(variable)。若有人想重命名var,那是与本提案正交的独立讨论,应分开进行。
四、平滑落地:四阶段推出计划
为了让社区迁移更平滑、破坏性更小,提案设计了一个分阶段推进路线:
- 阶段一——社区共识:与 Mojo 社区建立共识,收集反馈与额外视角;
- 阶段二——工程验证与警告期:完成工程工作,验证不存在性能回退,并移除
let的 IR 表示与行为。此阶段仍保留对let的解析以维持兼容:将其解析为与var相同的 IR,但发出警告"let已被弃用,将在下个版本移除",并附带把let重命名为var的 fixit 提示; - 阶段三——约 1 个月后:把警告升级为错误;
- 阶段四——约 1 个月后再过一版:连同错误信息一起,彻底移除该关键字。
五、替代方案:保留并日后重估
提案也考虑了替代方案:可以一直保留 let 并在日后重新评估。但作者判断届时不会有任何变化——Modular 内外部的 Mojo 用户社区已经多次踩到这些问题,这一问题会持续出现。该提案最终被标记为 Accepted(已接受),并在后续版本中落地。
六、仓库实证:提案在发布记录中的真实落地过程
原提案是设计蓝图,而当前仓库的 release notes 则记录了它的完整执行轨迹——恰好严格遵循了"警告 → 错误 → 彻底移除"的三步节奏:
v0.24.1(编译器移除 let 声明支持):v0.24.1.md 的 Changed 章节写明:"作为移除 let 声明的一步,我们已在编译器内部移除对 let 声明的支持。为便于迁移,我们把 let 声明解析为 var 声明,因此你的代码不会损坏。我们会对此发出警告,但请显式改用 var,因为该迁移支持将在后续更新中移除。"并给出示例:
fn test():
# 被当作 var 处理,但请更新你的代码!
let x = 42 # 警告:'let' 正在被移除,请改用 'var'
x = 9
v0.24.2(警告升级为编译期错误):v0.24.2.md 记载:"let 声明现在产生编译期错误而非警告,这是我们移除 let 声明的下一步。编译器暂时仍识别 let 关键字,以便产生良好的错误信息,但将在后续版本中移除。"
v0.24.4(关键字从语法中彻底消失):v0.24.4.md 宣告终点:"let 关键字已从语言中完全移除。我们此前已移除 let 声明但仍保留错误信息,现在它彻底从语法(grammar)中消失。"
这一演进路径与提案第五节"Rolling out this proposal smoothly"的设计完全吻合:先在编译器内部消掉语义(解析为 var 的 IR + 警告 + fixit),再升格为错误,最后连关键字与报错一起清除。
七、现状观察:var 与 alias 的现代 Mojo 写法
从当前仓库的代码可以直观验证提案落地后的语言形态——现代 Mojo 代码中不再出现 let 声明,命名值统一由 var(运行时可变值)与 alias(编译期值)承担。
例如标准库中的 rebind.mojo 展示了 var 在真实实现中的用法(var lit = ...、var rebound = ...),以及通过参数约定(var src: src_type, out dest: dest_type)表达可变/输出语义的方式。而 alias 则在 Mojo/stdlib/std 各模块(如 anytype.mojo、simd.mojo、dtype.mojo 等)中被大量使用,承担编译期常量与类型别名的职责。
由此可以确认提案落地后的清晰分工:
var:声明一个显式、词法作用域、可重新赋值的运行时值;alias:声明编译期值(常量、类型别名、编译期元数据);- 不可变性:不再以"具名不可变变量"形式出现,而是交由借用参数(
borrowed)与引用语义表达; def函数:保留 Python 风格的隐式变量声明。
对于从旧版本 Mojo 迁移的开发者,迁移路径同样清晰:把 let x = ... 直接改写为 var x = ...(语义等价,只是不再有不可变保证),把原本想表达编译期常量的 let 改为 alias;若确实需要不可变的借用视图,则依赖借用参数与未来的引用/生命周期特性(参见 lifetimes-and-provenance.md)。
结语:一次教科书式的语言简化决策
remove-let-decls 提案的价值不只在"删掉一个关键字"本身,更在于它示范了 Mojo 语言设计的方法论:以 Python 程序员的心智模型为锚点,以编译器复杂度为成本核算,以社区反馈为验证闭环,以分阶段发布控制迁移阵痛。从 提案文档 到 v0.24.4 发布说明 的完整闭环,既是 Mojo 早期语言演进的一段真实历史,也是理解现代 Mojo 语法体系中 var 与 alias 分工的最佳切入点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python270
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46066
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20143
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java34051