Roc 表达式语言参考:从取值、引用计数到编译期求值的完整解析
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
本文围绕 Roc 语言参考中的表达式(Expressions)章节展开,系统梳理「什么是表达式」「表达式的全部种类」「值与内存模型」「块表达式」「求值规则(含副作用与编译期求值)」这五条主线。读完后,你将理解 Roc 为什么把内存地址隐藏为底层细节、引用计数为何不需要处理引用环、编译器如何对共享值做就地修改(opportunistic mutation),以及哪些表达式可以在编译期被直接求值、从而在生成的二进制中只保留一份结果。
一、什么是表达式:判据与速查
在 Roc 中,表达式(expression)是可以求值为一个值(value)的东西。判断一个语法结构是不是表达式,有两个非常实用的判据:
- 括号判据:合法的表达式可以被括号包起来而不改变其含义;非表达式被括号包起来会直接报错。
- 赋值判据:表达式总是可以被赋予一个名字,也就是总能放在
=符号的右边。
文档给出了清晰的正反例,值得完整保留:
| 语法结构 | 是否表达式 | 说明 |
|---|---|---|
x | 是 | 求值为一个值;(x)合法 |
foo(1) | 是 | 求值为一个值;(foo(1))合法 |
1 | 是 | 本身已是值;(1)合法 |
import Foo | 否 | 这是语句(statement),(import Foo)非法 |
# Something | 否 | 这是注释(comment),(# Something)非法 |
package […] | 否 | 这是模块头(module header),(package […])非法 |
说明:
statements、comments-and-docs、modules等均为语言参考中的兄弟章节,位于docs/langref/目录下。上表中的相对链接已按仓库根目录路径规整,便于在仓库内直接定位。
二、Roc 的全部表达式种类
Roc 的表达式种类是封闭且穷尽的——语言参考明确声明「不存在其他类型的表达式」。完整清单如下:
- 字符串字面量,如
"foo"或"Hello, ${name}!" - 数字字面量,如
1或2.34或0.123e4 - 列表字面量,如
[1, 2]或[]或["foo"] - 记录字面量,如
{ x: 1, y: 2 }或{}或{ x, y, ..other_record } - 标签字面量(Tag literals),如
Foo或Foo(bar) - 元组字面量,如
(a, b, "foo") - 函数字面量(lambda),如
|a, b| a + b或|| c + d - 查找(lookups),如
blah或(blah)或$blah或($blah)或blah!或(blah!) - 调用(calls),如
blah(arg)或foo.bar(baz) - 运算符应用,如
a + b或!x,它们会脱糖为调用(desugar to calls) - 块表达式(block expressions),如
{ foo() }
这个封闭清单对静态分析、编译器实现和工具链(例如 LSP、格式化器)都极其友好:你只需覆盖这 11 类,就能遍历任意 Roc 源码中的表达式子树。从仓库源码结构看,解析器与语义检查分别位于src/parse/、src/check/等目录,正是围绕这组封闭的表达式 AST 节点进行递归处理。
三、值(Values)与内存模型
一个Roc 值是语义上不可变(semantically immutable)的一块数据。围绕「值」,文档展开了四个关键子话题,它们共同构成了 Roc 的内存模型。
3.1 值的身份(Value Identity):地址是幕后细节
因为值在运行时占据内存,每个值都有一个内存地址。但 Roc有意把内存地址视为幕后实现细节,设计上不暴露任何语言级的、用于访问或比较地址的手段。由此推出:
- Roc没有值身份(value identity)的概念;
- 没有引用相等(reference equality / physical equality);
- 没有指针(pointers)概念。
这些都是基于内存地址的语义概念,被有意排除在语言表层之外。文档同时保留了一个重要注记:平台作者(platform authors)可以自行实现基于地址的特性,因为平台能访问更低层语言、能看到平台收到的任何 Roc 值的地址。是否让平台使用者开始关心内存地址,是平台作者自行权衡的事,而语言本体始终把这些细节藏在幕后。
3.2 引用计数(Reference Counting)
堆上分配的 Roc 值会被自动引用计数,并且是原子的(atomically,为线程安全)。
哪些值在堆上、哪些在栈上,是引用计数的边界:
- 堆上分配、参与引用计数:字符串(strings)、列表(lists)、盒子(boxes)、递归标签联合(recursive tag unions)。
- 栈上分配、不参与引用计数:数字(numbers)、记录(records)、元组(tuples)、非递归标签联合(non-recursive tag unions)。
这意味着「是否需要引用计数」是一个与类型结构相关的静态事实:只有可能递归、需要堆存储的结构才会引入引用计数开销。从源码结构看,src/base/、src/layout/、src/values/等目录承载了布局、值表示与分配相关逻辑,与文档描述的「堆/栈分类 + 引用计数」相互印证。
3.3 引用环(Reference Cycles):设计上不可表达
其他语言支持的引用环,会给引用计数系统带来麻烦,通常的解法是引入运行时追踪式垃圾回收器(tracing GC),或引入弱引用(weak references)的概念。Roc 的设计选择是:语言层面根本无法表达引用环。既然环不可表达,上述那些补救手段就都不必要——这是 Roc 用「类型系统约束」换取「无 GC」的典型例子。
3.4 机会性修改(Opportunistic Mutation)
Roc 编译器采用 Perceus 提出的「functional-but-in-place」引用计数体系,做机会性修改。其规则:
- 对引用计数值的内建操作,当引用计数为 1 时就地(in place)修改;
- 当引用计数大于 1 时,先浅克隆(shallowly clone)一份,再修改并返回克隆体。
文档用一个具体例子说明:当List.set接收一个「唯一」的列表(引用计数为 1)时,该列表会被就地替换指定元素;当接收一个「共享」列表(引用计数不为 1)时,会先浅克隆列表,再在克隆体上替换指定元素。无论哪种情况,被修改后的列表都会被返回——调用者拿到的始终是修改后的列表,而至于实际改的是克隆体还是原对象,对使用者透明。
这种「语义不可变、物理上可复用」的设计,让纯函数风格代码在共享值上保持安全,同时在独占值上省去复制开销。
四、块表达式(Block Expressions)
块表达式是「在某个表达式之前带有若干可选语句」的表达式。它有自己的作用域:块内赋值的名字在块外不可访问;而整个块的求值结果,就是块末尾那个表达式的值。
因为语句是可选的,{ x }本身就是一个合法的块表达式。这在条件分支等场景里有 stylistic(风格上)的价值:
x = if foo { … } else { fallback }一个容易混淆的细节值得强调:
{ x, y }是记录(有两个字段,是{ x: x, y: y }的语法糖);- 但
{ x }永远是块表达式。
之所以这样设计,是因为在else { x }这类条件分支里,把{ x }解释为块表达式远比解释为「单字段记录{ x: x }」的语法糖更有用——单字段记录的出现频率远低于条件分支里对块的需求。
五、求值(Evaluation)
求值就是「表达式变成值」的过程。
5.1 严格求值(Strict Evaluation)
和大多数语言一样,Roc 使用严格求值(strict evaluation),不支持Haskell 等惰性语言那样的惰性求值(lazy evaluation)。
- 本身已经是值的表达式(如
4、"foo")会求值为其自身; - 更复杂的表达式(如函数调用、块表达式)可能需要多个步骤才能求值为一个值。
5.2 求值期间的副作用
规则是:通常只有对「带副作用函数(effectful functions)」的求值才能触发副作用,求值其他任何类型的表达式都不应产生副作用。仅有两个被明确允许的例外:
dbg语句:执行「把值记录到日志」的副作用,用于调试。它更像**步进调试器(step debugger)**那样的调试接口,不是程序语义的一部分,所以允许在带副作用函数之外产生副作用(正如步进调试可以作用于任意表达式,而不仅仅是调用带副作用函数)。expect语句:类似地执行「报告失败断言」的副作用。和dbg一样,这类输出是只面向程序员的,不属于程序语义。
文档还给出了平台作者相关的警告注记:谁负责内存的分配与释放,谁就可能在此期间执行副作用。平台作者可以自行决定在分配/释放内存时产生副作用,但这很容易让 Roc 应用作者感到意外,不应被依赖——因为 Roc 优化器假定内存分配/释放对程序没有可观察影响(除非分配失败),这意味着这类副作用在编译器不同补丁版本之间可能被以不同方式优化掉。
对应地,
dbg与expect作为语句,在语言参考中属于 Statements 章节。
5.3 编译期求值(Compile-Time Evaluation)
只要可能,Roc 编译器会把表达式在编译期求值,而不是留到运行时。
这仅对「只依赖编译期已知值」的表达式成立。以下内容不是编译期已知的:
- 从带副作用函数调用返回的值(可能随运行时状态变化);
- 平台提供的值。
若在编译期求值时遇到crash,它会像其他任何编译错误(语法错误、命名错误、类型不匹配)一样在编译期被报告。
一个特别有力的结论是:函数也是值。如果你通过调用其他顶层函数来定义一个顶层函数,那么所有这些工作都会在编译期完成:
make_adder = |amount_to_add| |num| num + amount_to_add add_one = make_adder(1) add_two = make_adder(2) add_three = |num| num + 3在这段代码里,add_one、add_two、add_three三个函数在编译出的 Roc 二进制中彼此等价(除了相加的数字不同)。前两个函数是「通过调用另一个函数定义出来的」这一事实无关紧要,因为那部分工作在构建期对add_one、add_two求值时就已经完成了。换言之,编译期求值把「定义时的计算」下沉到了构建时刻,运行时不再重复这部分工作。
六、小结与延伸阅读
本章把 Roc「表达式」相关的语言语义收敛为可操作的结论:
- 表达式种类是封闭的 11 类,配合「括号判据 / 赋值判据」可快速识别;
- 值语义不可变,但内存地址被隐藏,由此没有值身份、引用相等、指针;
- 引用计数按「堆/栈类型分类」划定边界,且通过「环不可表达」绕开了 GC 难题;
- 机会性修改让独占值就地更新、共享值浅克隆后再改,兼顾安全与性能;
- 严格求值 + 有限的副作用例外(
dbg/expect),以及尽可能下推到编译期的求值,共同决定了哪些计算发生在构建期而非运行时。
如需继续深入,可在同一语言参考目录下阅读 Statements、Functions、Operators、Records、Tuples、Tag Unions 等兄弟章节;语言参考总览见 docs/langref/README.md。
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考