错误处理 —— 可预见且可组合的健壮性 (Error Handling: Predictable and Composable Robustness)
程序在现实世界中难免会遇到各种预期之外的情况——文件没找到、网络连接中断、用户输入格式错误等等。如何优雅、健壮地处理这些“错误”情况,是衡量一门编程语言成熟度的重要标准。Flurry 在这方面下足了功夫,它利用其富有表现力的类型系统和专门的语法,提供了一套可预见且高度可组合的错误处理方案。
1. 表示“可能缺失”:可选类型 (?T
)
最简单的“错误”形式就是值的缺失。一个函数可能找到一个结果,也可能找不到。对于这种情况,Flurry 使用可选类型 (Optional Type),在类型前加上问号 ?
来表示:
-- 查找用户,可能找到 (User),也可能找不到 (null)
fn find_user(id: Uuid) -> ?User {
-- ... 查询逻辑 ...
if found {
user -- 自动提升为 ?User (Some(user))
} else {
null -- 表示缺失
}
}
test {
let user_opt = find_user(some_id);
-- 处理可选值通常用模式匹配
if user_opt is {
user? => println("Found user: {}", user.name),
null => println("User not found."),
}
let user_name = find_user(some_id)?.name.unwrap_or("Unknown");
}
?T
类型表示一个值要么是T
类型,要么是表示“无”的null
。- 处理
?T
通常使用模式匹配 (if is { some? => ..., null => ... }
) 来安全地访问其内部值。 - 这避免了 C/C++ 中空指针解引用的风险,强制开发者处理值可能缺失的情况。
2. 表示“可能失败”:错误联合类型 (!ErrorType ResultType
)
当一个操作不仅可能缺失结果,还可能因为特定原因失败时,我们需要更精确地表达“哪种错误”发生了。Flurry 使用感叹号 !
前缀来定义错误联合类型 (Error Union Type):
-- 读取配置文件,可能成功返回 Config,也可能因 NetErr 失败
fn load_config_from_network(url: String) -> ![NetErr, ParseErr] Config {
let response = http.get(url)!;
parse(response)!; -- 可能失败
}
-- 调用点处理
let config_result = load_config_from_network("...")! {
-- this catch branch runs before all other branches
catch e => println("Error loading config: {}", e'tag_name),
.ParseErr.RequiredFieldMissing(info) => println("Missing required field: {}", info),
.NetErr.Timeout => println("Network timeout!"),
.NetErr.ConnectionRefused => println("Connection refused!"),
.NetErr.* => println("Other network error."),
.ParseErr.* => println("Other parse error."),
-- this catch branch runs after all other branches
catch e => panic("Unhandled error: {}", e'tag_name),
}
3. 组合多种错误:层级化枚举与枚举融合
一个复杂的操作往往可能因为多种不同来源的错误而失败。例如,处理用户上传的文件可能遇到网络错误、文件系统错误、解析错误等等。为每种组合都定义一个新的错误类型会非常繁琐。Flurry 利用其强大的层级化枚举和枚举融合特性来优雅地解决这个问题:
-
库定义各自的错误枚举:
-- network_lib enum NetErr { Timeout, ConnectionRefused, InvalidResponse, Unauthorized, ... } -- filesystem_lib enum FsErr { NotFound(path: fs.Path), PermissionDenied(path: fs.Path), DiskFull(path: fs.Path), ... } -- parser_lib enum ParseErr { InvalidFormat(location: Location), RequiredFieldMissing(field: String), FieldTypeMismatch(field: String, expected: String, actual: String), }
-
函数签名中使用错误列表: 函数签名可以列出所有可能发生的错误类型。
-- 这个函数可能返回 Data,或者失败并返回 FsErr 或 ParseErr 中的任何一种 fn process_local_file(path: String) -> ![FsErr, ParseErr] Data { let content = fs.read(path)!; -- '!' 传播 FsErr let data = parser.parse(content)!; -- '!' 传播 ParseErr data }
-
融合类型消减规则 (Error Fusion Reduction): 设错误处理器可处理的效应集为A, 数据的效应集为B,则将处理器应用与计算后,数据剩余的错误集为B - A。也就是说,处理器会消解掉它所处理的错误,使得最终的计算结果不再携带这些错误。
fn complex_operation() -> ![FsErr, ParseErr, NetErr] Data { let path = network.download_path()!; let data = process_local_file(path)!; data }
这个消减规则使得错误处理流更加精确和可静态分析。
4. 为类型定义错误处理能力 (handles error [...] -> A { ... }
)
类似于代数效应的预制处理器,Flurry 也可能提供一种方式,让类型能够预定义处理特定错误类型集合的方法。这可以用于实现自定义的错误恢复策略或将错误转换为另一种形式。
- 语法:
struct SrcManager { -- 实际上,我们让这个处理器更像一个中间件 handles error [FsErr, ParseErr] -> ParseErr { .FsErr.NotFound(path) => println("File not found: {}", path), .ParseErr.UnexpectedToken(span, expected, context, actual, note) => self.report( .err, span, fmt.format("unexpected token `{}`, found {}, when parsing {}", self.src_content(actual), expected, context), note, .extra_lines = 3, ) ... } } test { -- ![ParseErr] Ast == !ParseErr Ast -- parse: fn(String) -> ![ParseErr] Ast -- parse(src).use(SrcManager): ![ParseErr] Ast -- parse(src).use(SrcManager)!: Ast let ast = parse(src).use(SrcManager)!; }
总结
Flurry 的错误处理系统巧妙地结合了多种机制,旨在实现类型安全、可组合和富有表现力的错误管理,避免了 C 的错误码、Java 的检查异常(带来的样板代码)和 Go 的 if err != nil
(重复代码)的许多缺点,同时提供了比 Rust(需要手动定义错误枚举或使用 anyhow
/thiserror
)更原生的多错误组合能力。它使得编写健壮的系统级代码变得更加容易和可靠。