在主流编程语言中,引用传递与值传递的边界往往模糊且充满陷阱。Python 的可变对象默认共享引用,C++ 的引用参数可以 const 限定但仍可能产生别名,JavaScript 的对象传递更是暗礁密布。这种混合语义导致的问题 —— 意外的副作用、难以追踪的数据竞争、复杂的内存管理 —— 几乎每个开发者都曾遭遇。Jcparkyn 的 herd 语言提供了一个激进的设计方案:彻底消除引用传递,一切皆为值。本文将剖析其编译器层面的实现策略,以及这一设计选择背后的工程权衡。
纯值语义的核心承诺
herd 对开发者做出了一项简洁而有力的保证:当你将任何值(包括列表和字典)传递给函数时,调用者持有的副本永远不会被子函数修改。这不是通过防御性拷贝或复杂的别名分析来实现,而是语言层面的硬性约束。换言之,herd 取消了「共享可变状态」这一概念在函数调用边界上的可能性。
这一保证的实现基础是引用计数与写时复制(Copy-on-Write,COW)的组合策略。所有引用类型 —— 字符串、列表、字典 —— 在内部都维护一个引用计数。当程序复制一个引用类型时,计数递增;当修改一个引用类型时,语言运行时检查当前引用计数:若计数为唯一引用,则直接原地修改,避免任何内存分配;若存在多个引用,则执行浅拷贝,将修改应用于新副本,使新副本的计数回归为一。这种策略使得后续修改通常不再需要额外分配,从而在保证语义安全的同时控制内存开销。
引用计数的另一个附带收益是消除引用循环的可能性。既然所有数据关系都是值的复制而非引用的绑定,就不可能形成 A 持有 B、B 又持有 A 的循环结构。这意味着 herd 的垃圾回收器无需复杂的循环检测算法,单纯的引用计数递减即可正确回收所有不可达对象。
类型系统与语法的协同设计
herd 采用动态类型,这一选择初看似乎与「安全」的工程目标相悖。但作者的解释揭示了更深层的设计考量:动态类型降低了语言实现的复杂度,使创作者能够专注于值语义这一核心创新,而非类型推导或边界检查的工程负担。更重要的是,值语义本身已经提供了相当程度的程序正确性保障 —— 函数无法偷偷修改其接收的参数,数据的流向在调用链上清晰可追踪。
在语法层面,herd 用三个关键词区分变量的生命周期状态。= 定义不可变变量,这是默认和推荐的方式;var 定义可变变量,需要显式声明;set 则用于修改变量的值。这种设计反映了作者对程序行为的认知:大多数变量在定义后确实不需要改变,将不可变设为默认可以减少思考负担,也使得代码的推断更加直接。函数定义采用反斜杠语法 \x y -> x + y,参数列表与函数体之间没有冗余的括号或箭头,代码密度高但语义清晰。
类型系统目前仅包含七种基本类型:unit(空值)、bool、number(64 位浮点数)、string、list、dict 和 function。值得注意的是,列表和字典作为复合类型,同样遵循值语义 —— 复制列表时得到的是独立的副本,修改副本不影响原列表。这种一致性消除了 JavaScript 中「原始类型 vs 对象」的心智模型分裂。
性能优化的工程实践
动态解释型语言常常与慢速划等号,但 herd 通过几项关键技术实现了令人意外的性能水平。其运行时使用 Cranelift 生成优化的机器码,执行时并非逐行解释,而是将代码编译为本地机器指令。Cranelift 是一个面向性能的代码生成后端,设计目标是快速编译与高质量输出的平衡,这使得 herd 能够在启动时完成代码编译,而不会产生显著的解释开销。
值表示采用 NaN-boxing 技术,这是一种在动态语言运行时中常见但实现精妙的技巧。JavaScriptCore 和 LuaJIT 等高性能引擎都采用类似方案。其核心思想是利用 IEEE 754 浮点数中 NaN 编码的冗余空间:在 64 位浮点表示中,指数位全为 1 且尾数位非零时表示 NaN,而语言运行时可以将这种位模式重新解释为指针或标记值,从而在单一机器字中存储原始类型(数字、布尔、空值)或堆对象的引用。这种表示方式避免了分离的标记指针结构,减少了内存占用并提高了缓存局部性,对数值密集型代码尤其有益。
当前的性能瓶颈主要来自三个方面:原子引用计数在多线程环境下的同步开销、单遍 JIT 编译器限制了跨函数的优化机会,以及标准库随代码规模增长带来的编译时间累积。对于原子操作的优化,作者指出可以通过将原子操作从热循环中「提升」出来来解决 —— 因为在单线程执行上下文中,引用计数的变化是可预测的,编译器可以安全地去除不必要的同步。
并发安全与设计局限
值语义对并发编程的影响是深远且正面的。由于每个线程接收到的都是数据的独立副本,线程间不可能通过共享引用产生数据竞争。即使在多线程并行处理同一数据结构的场景下,herd 也天然免疫于竞态条件:每个线程的修改只影响其本地副本,最终通过返回值汇聚结果。这一特性使得并行编程的模型大幅简化 —— 无需锁、无需原子操作、无需消息队列,只需关注计算本身。
然而,值语义也带来了通信开销。线程间唯一的数据交换通道是函数返回值,这意味着需要精心设计数据的序列化和反序列化路径。对于大型数据结构,完全拷贝的开销可能显著高于共享内存方案。此外,herd 目前不支持协程或异步执行模型,所有并行操作都是阻塞式的,这限制了 I/O 密集型应用的表达能力。
从生产可用性的角度审视,herd 作为个人项目明确声明不应用于关键系统。其类型系统缺少用户定义类型、模式匹配的完整支持以及完善的错误处理机制,这些都是工程语言必需的组件。作者设想未来可能添加类似 Julia 的 struct 和多重分派,但这些扩展必须与现有的值语义保证相兼容 —— 一个用户定义的复合类型,其字段的修改同样应遵循 COW 语义,这在类型系统的设计中增加了额外的约束。
设计哲学的启示
herd 的价值不在于它是一个完善的语言,而在于它示范了一条激进的简化路径。大多数语言在「安全」与「性能」之间寻找平衡点,引入各种复杂机制 —— 只读引用、不可变视图、逃逸分析 —— 来缓解引用传递的副作用。herd 的回答则是消除问题本身:如果所有数据都是值,就不需要复杂的别名分析或可变边界检查。
这种设计选择也呼应了函数式编程的核心洞见,但 herd 没有采用惰性求值或纯函数的强制约束。它保留了命令式风格的局部状态(通过 var 和 set),只是将状态的作用域限定在单个执行上下文内。程序员仍然可以写出熟悉的迭代和副作用代码,只是这些副作用不会「泄漏」到调用者的视野中。
从编译器工程的视角,herd 展示了值语义的实现成本是可接受的:引用计数加上浅拷贝的运行时开销,通过写时复制的优化可以控制在合理范围;内存占用由于引用计数字段的存在略有增加,但消除了循环检测的复杂度。综合来看,这一设计在语义清晰性、实现复杂度和运行时性能之间达到了一个有趣的平衡点。
参考资料
- GitHub: Jcparkyn/herd