Roc 静态分派完全解析:方法、Well-Known Methods 与编译器推导实现
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
Roc 是唯一把 ad hoc(即席)多态建立在**静态分派(Static Dispatch)**之上、并且刻意不支持动态分派的语言。本篇基于语言参考文档 static-dispatch.md 展开,讲清楚三件事:方法(method)如何定义与调用;语言内置的 well-known methods 如何把==、+、..<、字面量、for循环等语法“钩”到具体实现上;编译器如何为is_eq、to_hash、map等六种方法自动推导实现。读完你可以为自己的名义类型(nominal type)设计完整的运算符、字面量转换、遍历与编解码支持,并理解这些能力在编译器中的落地位置。
为什么 Roc 只做静态分派
*分派(Dispatch)*指同一个调用表达式可能根据参数/返回值的类型运行到不同函数,这是 ad hoc 多态的一种形式。分派又分两类:
- 静态分派:只依据编译期已知的类型决定运行哪个函数;
- 动态分派:利用运行时信息决定。
Roc 的设计立场是:静态分派是唯一的 ad hoc 多态机制,动态分派被有意排除。核心理由是零运行时开销——编译完成后,一次分派调用“就等价于你直接调用了那个函数”。而动态分派必须在运行时处理分派信息,运行时开销无法避免。
这一结论直接体现在源码结构上:类型检查阶段(src/check/Check.zig)在求解约束时就把每个使用点解析到具体的方法实现,后续代码生成阶段发出的是直接调用,或是对结构体派生的结构操作,不存在 vtable、trait 对象或类型标签查表之类的运行时机制。
方法:与类型绑定的函数
*方法(method)*是与类型关联的函数。在名义类型上,方法定义在类型声明之后的.{ }块里:
Counter := { value: I64 }.{ new : () -> Counter new = || { value: 0 } increment : Counter -> Counter increment = |{ value }| { value: value + 1 } }调用时使用“类型名.方法名”的显式形式:
counter : Counter counter = Counter.new().increment()文档特别强调了一个关键语义:大多数方法名只在被显式调用时才起作用;只有少数名字会被语言语法或内建 API 识别。定义了其中一个 well-known method,就相当于让该类型“opt-in”到对应语法或 API,而底层仍然走的是普通静态分派——不是动态接口。
源码可以印证这一点。src/canonicalize/ModuleEnv.zig 中,检查器维护了一张 well-known 方法名的标识符表(is_eq、range_exclusive_to、to_inspect、from_numeral、from_quote等),并在初始化时逐一插入:
.is_eq = try common.insertIdent(gpa, Ident.for_text("is_eq")), .range_exclusive_to = try common.insertIdent(gpa, Ident.for_text("range_exclusive_to")), ... .from_numeral = try common.insertIdent(gpa, Ident.for_text("from_numeral")), .from_quote = try common.insertIdent(gpa, Ident.for_text("from_quote")),检查阶段遇到运算符或字面量时,就是按这些“已知名字”去用户类型的环境里查找方法,解析出唯一实现后生成直接调用。
Well-Known Methods 全表
文档明确说明:这些方法不是动态接口。检查器把每一处使用解析到具体方法实现,之后的编译阶段发出直接调用或派生的结构操作。这张表也不是方法名限制——包可以用where子句定义并要求自己的方法。
| Method | Used by | Implement when |
|---|---|---|
to_inspect : T -> Str | Str.inspect(value) | 类型需要自定义调试表示 |
is_eq : T, T -> Bool | ==、!= | 需要可用或自定义的相等性 |
to_hash : T, Hasher -> Hasher | Dict、Set及基于哈希的 API | 值应参与哈希 |
plus、minus、times、div_by、div_trunc_by、rem_by | +、-、*、/、//、% | 类型具有类算术操作 |
is_lt、is_lte、is_gt、is_gte | <、<=、>、>= | 类型具有序关系 |
range_exclusive_to : T, T -> Range(T)、range_inclusive_to : T, T -> Range(T) | ..<、..= | 类型支持 range 语法 |
range_exclusive_from : T, T -> Range(T)、range_inclusive_from : T, T -> Range(T) | Range.iter_rev | 类型的 range 可精确反转 |
range_iter | Range.iter、Range.iter_rev | 类型可遍历其存储的 range 表示 |
range_len_if_known | 数值 range 构造器、Range.step_by | 类型在可表示时能给出精确的U64计数 |
negate、not | 一元-、一元! | 类型有一元取负或按位取反 |
from_numeral : Num.Numeral -> Try(T, [InvalidNumeral(Str)]) | 目标类型为T的数字字面量 | 普通数字字面量语法应构造该类型 |
from_quote : Str -> Try(T, [BadQuotedBytes(Str)]) | 目标类型为T的带引号字面量 | 普通引号字面量语法应构造该类型 |
from_interpolation : Str, Iter((item, Str)) -> T | 目标类型为T的插值字符串字面量 | 插值应构造该类型 |
iter : T -> Iter(item) | for item in value | 类型应在for循环中可遍历 |
next | for循环的迭代步进 | 通常由Iter提供;集合作者一般实现iter |
parser_for : encoding -> (state -> Try({ value : T, rest : state }, err)) | 通用解析 API(如 JSON 解析) | 某格式应能解析该类型 |
encoder_for : encoding -> (T, state -> Try(state, err)) | 通用编码 API(如 JSON 编码) | 某格式应能编码该类型 |
map | 合格 tag union 中选定 payload 的映射 | 类型应支持纯 payload 变换 |
map! | 同上 | 类型应支持带效果的 payload 变换 |
编译器推导方法(Compiler-Derived Methods)
Roc 可以为六种方法推导实现:is_eq、to_hash、parser_for、encoder_for、map、map!。规则是:
- 结构类型:只要形状支持,自动获得对应的推导方法;
- 名义/不透明类型:必须对每个想要的实现显式 opt-in——在
.{ }块中声明该方法、并把类型注解写成_:
Model := { value : Str }.{ is_eq : _ to_hash : _ parser_for : _ encoder_for : _ }这里_的含义是让编译器推断方法类型并合成实现。它仅对上面六个方法名有效;其他无方法体的声明是错误,除非是 host 提供的平台声明。这一 opt-in 机制在 application、package、platform 模块中行为一致。
与之相对,提供方法体就表示“自定义实现”而不是请求推导。例如一个类型可以自定义is_eq,同时仍然推导它的to_hash和 codec 方法。
推导的map与map!适用于恰好有一个被选定的直接 payload的 tag union;map接纯变换,map!接带效果的变换:
Maybe(a) := [Just(a), Nothing].{ map : _ map! : _ }定制调试表示:to_inspect
to_inspect方法定制Str.inspect如何渲染值,适用于调试、日志与测试失败输出:
Color := [Red, Green, Blue].{ to_inspect : Color -> Str to_inspect = |color| match color { Red => "Color.Red" Green => "Color.Green" Blue => "Color.Blue" } }对Color值调用Str.inspect时即走Color.to_inspect:
red : Color red = Red Str.inspect(red) # "Color.Red"没有to_inspect时,Str.inspect回退到 Roc 内建的结构表示。
从源码结构看,检查器在需要 inspect 时构造的调用目标就是该类型的to_inspect方法——src/check/Check.zig 中存在以self.cir.idents.to_inspect为方法名构造调用、并通过ownerEnvForOriginModule回退到定义模块环境的逻辑(包括内建类型的处理分支),与文档“解析为直接调用”的描述一致。
相等性与哈希:is_eq与to_hash
is_eq定制==/!=的相等性检查:
Point := { x: I64, y: I64 }.{ is_eq : Point, Point -> Bool is_eq = |a, b| a.x == b.x and a.y == b.y }对Point值使用==时即调用is_eq:
p1 : Point p1 = { x: 1, y: 2 } p2 : Point p2 = { x: 1, y: 2 } expect p1 == p2 # calls Point.is_eq(p1, p2) expect (p1 != p2) == False对!=,Roc 调用is_eq后对Bool结果取反(即!=不单独派发方法)。
to_hash把值喂入一个Hasher:
to_hash : T, Hasher -> Hasher基于哈希的 API(如字典)会同时使用to_hash与is_eq——字典的 key 必须可哈希且可比较。若自定义了相等性,必须保证哈希与之一致:相等的值必须喂入相同的哈希数据。
Roc 可对支持的结构形状推导结构相等与哈希;当推导行为不是你要的行为、或名义类型需要对外暴露稳定的自定义定义时,再定义显式方法。
运算符分派:左操作数决定目标方法
二元算术运算符分派到左操作数类型上的方法;返回类型是左操作数的类型,但若方法签名允许,右操作数可以是不同类型。
Vec := { x: I64, y: I64 }.{ plus : Vec, Vec -> Vec plus = |a, b| { x: a.x + b.x, y: a.y + b.y } }+用于Vec值时即调用Vec.plus(v1, v2):
v1 : Vec v1 = { x: 1, y: 2 } v2 : Vec v2 = { x: 3, y: 4 } # v1 + v2 calls Vec.plus(v1, v2)算术运算符映射:
| Operator | Method |
|---|---|
+ | plus |
- | minus |
* | times |
/ | div_by |
// | div_trunc_by |
% | rem_by |
比较运算符分派到结果为Bool的方法,且两个操作数必须同型:
| Operator | Method |
|---|---|
< | is_lt |
<= | is_lte |
> | is_gt |
>= | is_gte |
range 运算符分派到结果为操作数类型Range的方法,两个操作数也必须同型:
| Operator | Method |
|---|---|
..< | range_exclusive_to |
..= | range_inclusive_to |
所有内建数值类型都定义了这些方法;自定义类型可以通过定义它们来支持 range 语法。以页码类型为例:
PageNum := { num : U32 }.{ range_exclusive_to : PageNum, PageNum -> Range(PageNum) range_exclusive_to = |start, end| Range.custom({ lower: start, upper: end, step: PageNum.{ num: 1 }, upper_bound: Exclusive, direction: To, len_if_known: Unknown, }) } pages : Range(PageNum) pages = first_page..<last_page要遍历该 range,PageNum还需要基于自身表示定义range_iter;定义两个_from构造器(range_exclusive_from/range_inclusive_from)则 opt-in 到Range.iter_rev。若类型的步进不能精确反转,可以省略它们。
一元运算符分派到参数与返回类型相同的方法:
| Operator | Method |
|---|---|
-x | negate |
!x | not |
注意上面Duration的例子展示了“右操作数可以是不同类型”的灵活性——times的第二参数是I64,因此Duration * I64合法:
Duration := { millis : I64 }.{ times : Duration, I64 -> Duration times = |duration, scale| { millis: duration.millis * scale } } longer : Duration longer = Duration.{ millis: 10 } * 3运算符检查逻辑可以直接在 src/check/Check.zig 中找到:一元运算符按操作构造对negate的调用(约 L23128),二元运算符则根据左操作数类型解析方法名(plus、minus等,约 L23239-L23318),解析失败时通过reportMissingNominalMethodForBinop报告“缺少名义类型方法”的诊断。
字面量转换:from_numeral、from_quote、from_interpolation
数字字面量在目标类型是定义了from_numeral的名义类型时分派该方法:
Celsius := { degrees: I64 }.{ from_numeral : Num.Numeral -> Try(Celsius, [InvalidNumeral(Str)]) from_numeral = |n| match I64.from_numeral(n) { Ok(degrees) => Ok({ degrees }) Err(err) => Err(err) } } temp : Celsius temp = 21 # calls Celsius.from_numeralNum.Numeral携带字面量的精确数字串,因此自定义类型可以接受自己表示范围支持的字面量、其余以InvalidNumeral拒绝。检查器会为检查过的from_numeral函数按数值表达式记录(见 src/check/Check.zig 中“Record/Return the checked from_numeral function for a numeric expression”的辅助逻辑)。
带引号字面量在目标类型定义了from_quote时分派它:
HttpMethod := [Get, Post, Put, Delete].{ from_quote : Str -> Try(HttpMethod, [BadQuotedBytes(Str)]) from_quote = |raw| match raw { "GET" => Ok(Get) "POST" => Ok(Post) "PUT" => Ok(Put) "DELETE" => Ok(Delete) _ => Err(BadQuotedBytes("expected GET, POST, PUT, or DELETE")) } } method : HttpMethod method = "POST" # calls HttpMethod.from_quote若方法返回Err(BadQuotedBytes(message)),编译器在程序运行之前就报告该字面量转换错误——这是字面量转换区别于运行时验证的关键:错误在检查期就暴露。
插值字符串字面量按结果类型分派from_interpolation。第一个参数是第一个插值之前的字面量段;迭代器依次产出“每个插值值与其后跟的字面量段”的配对:
# For a target type named Html: from_interpolation : Str, Iter((Html, Str)) -> Html插值中未插值的普通引号段始终是Str值;被插值的值则是Iter((item, Str))中的item类型。
遍历:iter与next
for循环会对in后的值调用iter。iter必须返回一个Iter(item),其 item 类型与循环模式匹配:
Rows := { items : List(Row) }.{ iter : Rows -> Iter(Row) iter = |rows| rows.items.iter() } for row in rows { process(row) }随后循环反复调用Iter(item)值上的next:
next : Iter(item) -> [One({ item : item, rest : Iter(item) }), Skip({ rest : Iter(item) }), Done]包作者通常为自己的集合类型实现iter,并用IterAPI 构造返回的迭代器;next则是迭代器值本身的钩子。
解析与编码:parser_for与encoder_for
通用解析器/编码器 API 使用parser_for和encoder_for询问类型“对某个特定格式应如何读取/写入”。方法体内部通过where子句约束 encoding 必须具备的能力,体现了“静态分派 + 能力约束”的组合:
Token := { raw : Str }.{ parser_for : encoding -> (state -> Try({ value : Token, rest : state }, err)) where [ encoding.parse_str : encoding, state -> Try({ value : Str, rest : state }, err), ] parser_for = |encoding| { Encoding : encoding |state| { parsed = Encoding.parse_str(encoding, state)? Ok({ value: Token.{ raw: parsed.value }, rest: parsed.rest }) } } encoder_for : encoding -> (Token, state -> Try(state, err)) where [ encoding.encode_str : encoding, Str, state -> Try(state, err), ] encoder_for = |encoding| { Encoding : encoding |token, state| Encoding.encode_str(encoding, token.raw, state) } }结构记录、tag union、list、set、dictionary 以及支持的内建类型,在所选格式支持其形状时可以使用推导的parser/encoder 实现;名义类型在需要自定义表示、或希望隐藏其底层结构时,提供显式的parser_for/encoder_for方法。
数字字面量默认类型(Number Literal Defaulting)
当程序中没有任何东西固定一个字面量的类型时,编译器按固定顺序提交第一个满足该字面量全部约束的类型:
Dec, I64, U64, I128, U128, I32, U32, I16, U16, I8, U8, F64, F32裸5默认为Dec;若周围代码要求整数,则取第一个适用的整数类型。若默认提交收窄了函数的推断类型,编译器会发出LITERAL DEFAULTED警告;要选其他类型,加类型注解或使用后缀(如5.U64)。
源码中这一机制的痕迹相当明确:src/check/canonical_type_keys.zig 的注释指出“numeral 默认 Dec、quote 默认 Str,且仅当字面量上的每个约束都……”时才提交默认;src/check/Check.zig 中围绕LITERAL DEFAULTED的注释(约 L26978 起)说明了 per-literal 的泄漏检查与警告区域的细节。
小结与延伸阅读
- 静态分派是 Roc 唯一的 ad hoc 多态:检查期解析到具体实现,编译产物即直接调用,无运行时开销;
.{ }方法块既承载普通方法(Counter.new()),也承载 20 余个 well-known 钩子,把运算符、字面量、range、for、inspect、编解码全部静态化;- 六个可推导方法(
is_eq、to_hash、parser_for、encoder_for、map、map!)在名义类型上用method : _显式 opt-in,其余方法必须提供方法体; - 文档中 “Where Clauses” 与 “Aliases” 两节目前标注 TODO,属于语言参考 langref 的在建部分,使用时以该文档后续更新为准;
- 相关语言参考章节:operators.md、numbers.md、iterators.md、tag-unions.md、records.md、strings.md、dictionaries-and-sets.md;
- 实现证据主要位于 src/check/Check.zig(方法解析与运算符分派)、src/canonicalize/ModuleEnv.zig(well-known 方法标识符表)。
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考