Flurry: 为下一代基础设施而生
欢迎来到 Flurry 的世界!Flurry 并非又一个通用编程语言,它的诞生承载着一个明确的使命:为构建未来高性能、高可靠性、高安全性的计算基础设施提供坚实的基础。
问题域:系统编程的困境
现代数字世界的基石——操作系统、网络协议栈、数据库、虚拟机、云平台——都依赖于系统级编程语言来构建。这些基础设施软件对正确性、性能和资源利用率有着极致的要求。然而,长期以来,主流的系统级语言,特别是 C 和 C++,虽然赋予了开发者无与伦比的底层控制能力,但也因其固有的内存安全问题和并发处理复杂性,成为了无数严重软件缺陷和安全漏洞的温床 1。
近年来,像 Rust 这样的语言通过引入所有权和借用检查等创新机制,在编译时提供了强大的内存和线程安全保证,极大地改善了现状。但这并非终点。为了实现必要的底层控制和极致性能,即使是 Rust 也需要 unsafe
代码块,这道“后门”重新引入了对形式化验证的需求,以确保安全边界的稳固2。此外,仅仅保证内存和线程安全是不够的,基础设施软件还需要深层次的功能正确性——即程序必须精确地按照其复杂规范运行。现有的验证技术在面对系统编程的复杂性(指针算术、细粒度并发、硬件交互、复杂的别名)时,往往捉襟见肘3。
我们相信,是时候需要一种新的系统级编程语言了——它不仅要提供现代语言的安全性与开发效率,还要在设计之初就内建对高级验证和复杂系统建模的支持,并提供无与伦比的编译时能力来消除抽象开销和实现深度优化。
Flurry 的答案:安全、性能与表现力的融合
Flurry 正是为此而设计的。它旨在成为构建下一代操作系统、网络栈、数据库内核、嵌入式系统以及其他关键基础设施的理想选择。Flurry 的核心理念是在以下几个关键维度上取得突破性的进展:
-
内建安全性与验证支持:
- 分级安全模型: 提供
unsafe
,safe
,verified
三个安全级别,允许开发者明确风险并逐步提升代码可信度。 - 创新的安全机制: 超越传统借用检查,探索结合仿射类型、可达性类型系统和副作用系统,旨在提供灵活且强大的静态安全保证。
- 面向验证的设计: 语言核心特性(如代数效应、
comptime
元编程)的设计考虑了与形式化方法(如 Outcome Separation Logic, Reachability Logic, Matching Logic)的集成可能性,目标是让深度验证更加可行。
- 分级安全模型: 提供
-
极致的编译时能力 (
comptime
):- 图灵完备的编译时执行: Flurry 拥有一个功能强大的编译时运行时,允许在编译阶段执行任意复杂的计算、代码生成和静态检查。
- 零成本抽象: 泛型、元编程和高级抽象通过编译时计算实现单态化和优化,不引入运行时开销。
- 编译时配置与构建: 包管理、特性标志、甚至构建逻辑都可以使用 Flurry 自身的
comptime
代码定义,提供无与伦比的灵活性。
-
富有表现力且一致的语法:
- 现代特性: 吸收并改进了代数效应、强大的模式匹配、Trait 系统、层级化枚举等现代语言特性。
- 创新语法: 引入
expr object
DSL 构建、expr ' image
取像、extend
作用域扩展、.tagged_polymorphic
多态等独特语法,旨在提升特定场景下的表达力和简洁性。 - 一致性: 追求语法元素(如
.
,^
,'
)在不同上下文中的一致语义。
-
性能与控制力:
- 系统级定位: 提供必要的底层控制能力(如指针操作,但需在
unsafe
中或得到验证)。 - 面向性能的设计: 编译时计算、潜在的优化(如
tagged_polymorphic
分派)都旨在生成高效的本地代码。
- 系统级定位: 提供必要的底层控制能力(如指针操作,但需在
为什么选择 Flurry?(Teaser)
- 超越 Rust 的安全边界: 在提供
safe
基础的同时,为unsafe
和功能正确性提供内建的、更强大的验证路径。 - Zig 级别的编译时能力,甚至更强:
comptime
是 Flurry 的核心支柱,深度集成到类型系统、元编程和构建过程中。 - 代数效应带来的结构化并发与控制流: 提供比传统回调、Promise、async/await 更灵活、更可组合的副作用和控制流管理。
- 独特的 DSL 构建能力:
expr object
和extend
字面量拓展等特性使得构建嵌入式领域特定语言极其自然。 - 为复杂系统而生的模块化与验证支持: 从包管理到形式化方法集成,都旨在应对大型、关键系统的挑战。
Flurry 是一项雄心勃勃的探索,它试图站在现有系统语言的肩膀上,融合最新的编程语言理论研究,为未来构建更安全、更可靠、更高效的数字基础设施提供下一代利器。我们邀请您一同踏上这段激动人心的旅程!
-
Memory-Safe Programming Languages and National Cybersecurity ... ↩
-
Surveying the Rust Verification Landscape - arXiv ↩
-
Concurrent Separation Logic ↩
快速入门
本章旨在让您快速领略 Flurry 语言的风采。我们将通过一个简单的示例展示 Flurry 的一些核心特性,并指导您完成基本的安装和运行流程。我们的目标不是详尽解释所有细节,而是让您对 Flurry 的编程体验有一个初步的感受。
"Hello, Infrastructure!" - 一个 Flurry 示例
让我们来看一个稍微超越传统 "Hello, World!" 的例子,它将触及 Flurry 的编译时计算和可选类型:
-- src/main.fl
-- 使用标准库的打印功能
use std.io.println;
-- 使用构建信息模块 (假设存在)
use build;
-- 定义一个编译时常量,存储构建目标操作系统
const TARGET_OS: str = comptime build.target.os'tag_name; -- 取像操作获取 tag 名
-- 程序入口点 main 函数
pub fn main() -> void {
let user_name: ?String = get_user_from_env(); -- 返回可选类型 ?String
-- 编译时条件分支 (inline if)
inline if TARGET_OS == "windows" {
println("Detected Windows!");
} else if TARGET_OS == "linux" {
println("Running on Linux!");
} else {
println("Running on an unknown OS: {str}", TARGET_OS); -- 字符串插值
}
-- 处理可选类型
if user_name is {
some name => { -- 模式匹配解构 Some
println("Hello, {str}!", name); -- {str} 是格式化说明符
}
null => { -- 匹配 null
println("Hello, anonymous user!");
}
}
}
-- 假设的函数,从环境变量获取用户名,可能失败
fn get_user_from_env() -> ?String {
-- ... (实际实现会读取环境变量) ...
-- 模拟可能找到或找不到
if std.random.bool() { -- 假设有随机库
"FlurryDev".to_string() -- 返回 String,自动提升为 ?String
} else {
null -- 返回 null
}
}
这个简单的例子展示了:
- 模块导入 (
use
): 如何引入外部模块的功能。 - 编译时计算 (
comptime
): 如何在编译时获取信息(如构建目标)并将其用于代码逻辑。 - 取像操作 (
'tag_name
): 如何获取一个值(这里是编译时符号)的元信息。 - 编译时条件 (
inline if
): 如何根据编译时条件选择性地包含代码。 - 可选类型 (
?String
): 如何表示可能不存在的值。 - 模式匹配 (
if is { some => ..., null => ... }
): 如何安全地处理可选类型。 - 字符串插值 (
"{str}"
,$variable
): (假设${}
用于编译时插值,{str}
用于运行时格式化)。 - 自动类型提升:
String
和null
如何根据函数返回类型?String
自动提升。
核心特性概览 (亮点速览)
除了上面的例子,Flurry 还拥有更多令人兴奋的特性:
- 代数效应 (Algebraic Effects): 以结构化、可组合的方式处理 IO、并发、错误等副作用。
effect Ask(question: String) -> String; -- 定义效应 fn greet() -> #Ask void { -- 标记函数可能发出 Ask 效应 let name = perform Ask("What's your name?"); -- 发出效应 println("Hello, {str}!", name); } greet() # { -- 提供 Handler Ask(q) => { println("Answering: {str}", q); resume "Flurry"; } -- 处理效应并恢复 }
- 强大的模式匹配: 超越简单的值匹配,支持解构、范围、守卫、类型匹配等。
match response { { status: 200, body } => process_ok(body), { status: 404 } => log_not_found(), { status: 4xx } if is_client_error(status) => handle_client_error(status), -- 范围/通配符与守卫 _ => handle_other_error(), }
expr object
DSL 构建: 以极其声明式的方式构建复杂数据结构。-- (之前 Web API 示例中的查询或响应构建) let query = Product.query { .limit 10, .sort "price", .filter <{ category: "books", price: ..100.0 }>, -- <{...}> 模式字面量 }
extend
与字面量拓展:extend u64 { fn kb(self) -> usize { self * 1024 } } let buffer_size = 4kb; -- 等于 4 * 1024
- 类型即代码 (
comptime
): 在编译时操作类型、生成代码。-- 生成特定大小的数组类型 comptime let MyArrayType = Type.Array(i32, 10); let arr: MyArrayType = uninitialized;
这仅仅是冰山一角!Flurry 的设计充满了各种精心设计的特性,旨在让系统编程更安全、更高效、更富有表现力。
安装与运行 (暂定)
TODO: 在此部分添加关于如何获取 Flurry 编译器/工具链(例如,下载预编译版本、从源码构建)、设置开发环境以及编译运行第一个示例(hello_infrastructure
)的具体步骤。
例如:
- 下载: 前往 Flurry 官方网站/仓库 下载最新的编译器。
- 安装: 解压或运行安装程序,并将 Flurry 可执行文件路径添加到系统
PATH
。 - 验证: 打开终端,运行
flurry --version
确认安装成功。 - 创建项目:
mkdir hello_flurry && cd hello_flurry
- 创建
src/main.fl
: 将上面的示例代码粘贴进去。 - 创建
package.fl
(最简形式,假设需要):-- package.fl { .name "hello_flurry", .type .exe }
- 编译:
flurry build
- 运行:
.build/hello_flurry
(或 Windows 上的.build\hello_flurry.exe
)
(以上步骤是假设性的,需要根据实际的工具链进行调整。)
现在,您已经对 Flurry 有了初步的认识。准备好深入探索了吗?接下来的章节将带您详细了解 Flurry 的各项核心特性。
第 3 章:基础语法与数据类型
欢迎进入 Flurry 语言的核心基础。本章将引导您熟悉构成 Flurry 程序骨架的基本元素:词法结构、如何表示常量值(字面量)、语言内建的基本数据类型,以及 Flurry 特有的符号类型。理解这些基础构件是掌握 Flurry 编程的第一步。
Flurry 的设计在语法层面力求简洁和一致性,同时也引入了一些独特的规则(如运算符的空格敏感性和特定情况下的分号省略)来提升代码的可读性和减少歧义。
本章包含以下内容:
- 词法结构: 介绍构成 Flurry 代码的基本单元,如标识符、关键字、运算符和注释的规则。
- 字面量详解: 详细探讨如何在 Flurry 中表示整数、浮点数、字符、字符串、布尔值以及符号等固定值。
- 基本数据类型: 概述 Flurry 提供的内建原子数据类型,它们是构建更复杂数据结构的基础。
- 符号: 深入了解 Flurry 中独特的符号类型及其用途。
掌握这些基础知识,将为您后续学习 Flurry 更强大的特性(如表达式、控制流、函数和类型系统)打下坚实的基础。
词法结构
Flurry 代码由一系列遵循特定规则的词法单元 (Tokens) 构成。词法分析器 (Lexer) 负责将源代码文本流分解为这些有意义的单元。理解这些基本规则有助于编写语法正确的 Flurry 代码。
标识符 (Identifiers)
标识符用于命名变量、函数、类型、模块等。Flurry 的标识符必须遵循以下规则:
- 以字母 (a-z, A-Z) 或下划线 (
_
) 开头。 - 开头字符后可以跟任意数量的字母、数字 (0-9) 或下划线。
- 标识符是大小写敏感的(
myVariable
和myvariable
是不同的标识符)。
let user_name = "Alice"; -- 合法的标识符
const MAX_CONNECTIONS = 100;
fn _internal_helper() { ... }
-- let 1st_attempt = 0; -- 非法,不能以数字开头
-- let user-name = "Bob"; -- 非法,包含非法字符 '-'
关键字 (Keywords)
关键字是 Flurry 语言保留的、具有特殊含义的标识符,不能用作普通标识符。Flurry 拥有一套精心设计的关键字集合,以支持其丰富的特性。
TODO: 在此列出 Flurry 的完整关键字列表,或链接到附录中的关键字参考。
例如,fn
, let
, const
, struct
, enum
, if
, else
, while
, for
, use
, mod
, comptime
, impl
, trait
, unsafe
等都是 Flurry 的关键字。
运算符 (Operators)
Flurry 支持多种运算符,用于执行算术、逻辑、比较、赋值等操作。一些常见的运算符包括 +
, -
, *
, /
, %
, ==
, !=
, <
, >
, <=
, >=
, =
, .
等。
空格敏感性: Flurry 对某些二元运算符(+
, -
, /
, *
, %
, <
, >
)引入了空格敏感性规则,以减少歧义(特别是泛型尖括号 <
>
与比较运算符的混淆):
- 当这些运算符两侧都有空格时,它们被识别为标准的二元算术或比较运算符。
let sum = a + b; let is_greater = x > y;
- 如果至少一侧没有空格,它们会被词法分析器识别为不同的 token(例如,用于泛型或模板的尖括号)。
let list: Vec<i32>; -- '<' 和 '>' 两侧无空格,被识别为泛型分隔符 -- a+b -- 词法分析器可能不会将其识别为标准加法
这项规则鼓励开发者在二元运算中使用空格,提高代码可读性,并在词法层面解决常见的解析冲突。
注释 (Comments)
注释用于在代码中添加说明,它们会被编译器忽略。Flurry 支持两种类型的注释:
- 行注释 (Line Comment): 以
--
开始,直到行尾。-- 这是一个行注释 let timeout = 1000; -- 设置超时时间 (毫秒)
- 块注释 (Block Comment): 以
{-
开始,以-}
结束,可以跨越多行,并且通常支持嵌套。{- 这是一个块注释。 它可以包含多行文本。 {- 嵌套的块注释 -} -} let config = load_config();
分号 (Semicolons) 与语句结束
Flurry 使用分号 ;
来分隔或结束语句。然而,为了简洁性,Flurry 引入了一条特殊的分号省略规则:
- 当一个语句的最后一个 token 的最后一个字符是右花括号
}
时,该语句末尾的分号可以省略。
这常见于代码块(函数体、结构体定义、if
块等)或某些复合字面量(如 object
字面量)的结束处。
struct Point { x: i32, y: i32 } -- '}' 结尾,可省略 ';'
fn main() {
println("Hello"); -- ')' 结尾,不可省略 ';'
if true {
do_something() -- 函数体 '}' 结尾,可省略 ';'
} -- if 语句 '}' 结尾,可省略 ';'
} -- main 函数体 '}' 结尾,可省略 ';'
let data = { .key "value" } -- object 字面量 '}' 结尾,可省略 ';'
在不符合此规则的情况下,语句末尾必须使用分号。这条规则旨在提高常见代码模式的视觉整洁度,但并不改变语句的基本分隔原则。
理解这些基本的词法规则是编写和阅读 Flurry 代码的基础。
请检查这些内容是否符合您的预期,特别是关于可选参数默认 null
、do block、编译时求值和沉降机制的暗示,以及整体的语气把握。
字面量详解
字面量 (Literals) 是源代码中直接表示固定值的文本表示。它们是构建程序逻辑和数据的基本元素。Flurry 提供了丰富且灵活的字面量语法,以精确地表示各种常见类型的值。
1. 整数 (Integer)
整数用于表示没有小数部分的数值。
- 十进制 (Decimal): 最常见的形式。
123 0 -42
- 不同进制:
- 二进制 (Binary): 以
0b
或0B
开头。0b101010 -- == 42 0B1111_0000
- 八进制 (Octal): 以
0o
或0O
开头。0o777 -- == 511 0O123_456
- 十六进制 (Hexadecimal): 以
0x
或0X
开头。0xFF -- == 255 0xCafeBabe 0Xdead_beef
- 二进制 (Binary): 以
- 类型后缀 (Type Suffix): 可以通过后缀指定具体的整数类型。常见的后缀包括
u8
,i32
,u64
,isize
,usize
等(具体可用后缀依赖于语言标准库定义)。如果省略后缀,编译器会根据上下文尝试推断类型,或使用默认类型(例如i32
)。100i32 42u8 0xFF_u64
- 下划线分隔符 (
_
): 为了提高可读性,可以在数字之间使用下划线_
作为分隔符。下划线会被编译器忽略。注意,字面量的第一个字符不能是下划线(除非是负号后的第一个数字)。1_000_000 0b1111_0000_1010_0101
2. 浮点数 (Float)
浮点数用于表示可能带有小数部分的数值。
- 基本形式: 必须包含小数点
.
或指数标记e
/E
。3.14159 -0.5 100.0 -- 即使小数部分为 0,也表示浮点数 1. -- 合法,等同于 1.0 (如果语法允许) .5 -- 可能非法,通常需要小数点前有数字
- 科学计数法 (Scientific Notation): 使用
e
或E
表示 10 的幂。1.23e4 -- == 12300.0 -5.67E-3 -- == -0.00567
- 类型后缀 (Type Suffix): 可以使用
f32
或f64
后缀指定精度。如果省略,编译器通常会推断或默认使用f64
。2.718f32 -42.0f64 1e6f64 -- 1 百万,64 位浮点数
- 下划线分隔符 (
_
): 同样可用,规则与整数相同。3_141_592.653_589f64 1_000e-3_f32 -- == 1.0f32
3. 字符 (Character)
字符表示单个 Unicode 标量值,用单引号 ' '
括起来。
- 普通字符:
'a' 'Z' '7' '_'
- 转义序列 (Escape Sequences): 使用反斜杠
\
进行转义,支持常见转义:\'
: 单引号\\
: 反斜杠\n
: 换行符 (Newline)\r
: 回车符 (Carriage Return)\t
: 水平制表符 (Horizontal Tab)- (可能还有其他,如
\0
空字符等)
'\'' -- 表示单引号字符 '\\' -- 表示反斜杠字符 '\n'
- Unicode 转义: 使用
\x{...}
形式,花括号内是 1 到 6 位的十六进制 Unicode 码点。'\x{41}' -- == 'A' '\x{3A3}' -- == 'Σ' (希腊字母 Sigma) '\x{1F600}' -- 😀 (笑脸 Emoji)
4. 字符串 (String)
字符串表示一系列字符,用双引号 "
"
括起来。
- 基本字符串:
"Hello, Flurry!" "这是一个包含 Unicode 的字符串:你好 Σ 😀" "" -- 空字符串
- 转义序列: 字符串内部支持与字符字面量相同的转义序列。
"第一行\n第二行\t缩进" "路径是: \"C:\\Program Files\\\""
- 字面量合并 (Concatenation): 多个相邻的字符串字面量(仅由空白分隔)会被编译器自动合并成一个单一的字符串。这对于书写长字符串或跨行书写很有用。
let long_message = "这是第一部分, " "这是第二部分, " "这是最后一部分."; -- 等价于: let long_message = "这是第一部分, 这是第二部分, 这是最后一部分.";
- 原始字符串 (Raw Strings): 对于需要包含大量特殊字符(如正则表达式、代码片段、多行文本)而无需转义的情况,可以使用原始字符串,其通过
builtin.raw_str
宏提供,{}
内的所有内容(包括换行符)都将按原样成为字符串的一部分。-- 假设语法 let regex_pattern = builtin.raw_str'{^\d{4}-\d{2}-\d{2}$} let multi_line = builtin.raw_str'{ 这是一个多行文本, 内部的 "引号" 和 \ 反斜杠 都无需转义。 }
- 编译时类型: 需要特别指出的是,在 Flurry 中,字符串字面量
"..."
本身很可能属于编译时类型str
(或类似的代数字符串类型),它代表了编译时已知的文本内容。这使得它们可以用于comptime
计算和字符串插值。运行时的、可变的或堆分配的字符串可能是另一种类型(例如String
)。 - 编译时插值 (
$variable
): 字符串字面量支持使用$identifier
的形式嵌入编译时已知的变量或常量的值。
注意:这与运行时的字符串格式化(例如comptime const VERSION = "0.1.0"; const MESSAGE = "Welcome to Flurry version $VERSION!"; -- MESSAGE 的值在编译时确定为 "Welcome to Flurry version 0.1.0!"
println
中使用的{}
占位符)是不同的概念。 - 变体后缀 (Variant Suffixes): 字符串字面量后可以跟特定的标识符后缀来创建特殊类型的字符串表示:
"..."c
(C String Variant): 创建一个与 C 语言 ABI 兼容的字符串表示,通常意味着以空字符 (\0
) 结尾,并可能使用特定的编码(如 UTF-8)。用于 FFI 交互。"..."b
(Byte String Variant): 将字符串字面量的内容解释为一个字节序列(通常是Slice<u8>
或类似类型),忽略其文本编码。用于处理原始二进制数据。
5. 布尔值 (Boolean)
表示逻辑真或假。
true
: 真false
: 假
它们是 Flurry 的内置布尔类型 (bool
) 的唯二值。
6. null
字面量
null
是一个特殊的字面量,用于表示 Flurry 中可选类型 (?T
) 的“无值”状态。它本身可能没有独立的类型,而是作为可选类型的特殊值存在。
let maybe_value: ?i32 = null;
7. 符号 (Symbol)
符号是一种独特的字面量类型,以点 .
开头,后跟一个标识符。它们代表编译时唯一的标识符常量。具体细节将在 符号 章节中讨论。
.ok
.error
.my_custom_symbol
掌握 Flurry 丰富而精确的字面量表示法,是有效利用其类型系统和编译时能力的基础。
基本数据类型
基本数据类型 (Primitive Data Types) 是 Flurry 语言内建的最原子化的数据类型。它们构成了构建所有更复杂数据结构(如结构体、枚举、数组等)的基础。Flurry 提供了一组常见的、面向系统编程需求的基本类型。
TODO: 以下列表需要基于 Flurry 的最终设计进行确认和完善。
1. 整数类型 (Integer Types)
Flurry 旨在提供一系列固定位宽的整数类型,以进行精确的位级控制:
- 有符号整数:
i8
: 8 位有符号整数i16
: 16 位有符号整数i32
: 32 位有符号整数i64
: 64 位有符号整数i128
: 128 位有符号整数 (可选支持)isize
: 指针大小的有符号整数(其位宽与目标平台的指针位宽相同,通常是 32 位或 64 位)。常用于索引和大小计算。
- 无符号整数:
u8
: 8 位无符号整数 (常用于表示字节)u16
: 16 位无符号整数u32
: 32 位无符号整数u64
: 64 位无符号整数u128
: 128 位无符号整数 (可选支持)usize
: 指针大小的无符号整数。是数组索引、集合大小、内存偏移量等的首选类型。
2. 浮点数类型 (Floating-Point Types)
用于表示带有小数的数值,遵循 IEEE 754 标准:
f32
: 32 位单精度浮点数。f64
: 64 位双精度浮点数。
浮点数支持标准的算术运算。需要注意浮点数运算的精度问题和特殊值(如 f64.NaN
, f64.Infinity
)。
3. 布尔类型 (Boolean Type)
bool
: 表示逻辑值,只有两个可能的取值:true
和false
。 常用于条件判断和逻辑运算 (and
,or
,not
)。
4. 字符类型 (Character Type)
char
: 表示一个 Unicode 标量值 (Unicode Scalar Value)。它的大小通常是 32 位(以容纳所有 Unicode 标量值),但具体实现可能有所不同。用于表示单个字符。
5. void
类型 (Unit Type)
void
: 这个类型只有一个值(即unit()
)。它用于表示函数不返回任何有意义的值,或者用于泛型编程中表示空占位符。
6. 指针类型 (Pointer Types)
Flurry 提供了对指针的底层访问能力(尤其在 unsafe
上下文中),预计包含:
*T
: 指向类型T
的不可变原始指针 (Raw Pointer)。
原始指针的操作(解引用、算术)通常被认为是 unsafe
的,因为编译器不保证它们指向有效的内存或没有别名冲突。Flurry 的可达性类型系统和副作用系统旨在配合这些指针类型,提供比 C/C++ 更强的(即使在 unsafe
中)分析和潜在保证。
7. 编译时特定类型 (Compile-time Specific Types)
Flurry 的 comptime
世界可能还包含一些特殊的编译时类型:
Type
: 代表类型的类型。用于泛型、反射和编译时元编程。str
(编译时): 字符串字面量的编译时类型,代表已知的文本内容。Symbol
: 符号类型 (.id
),代表唯一的编译时标识符。Integer
,Real
(编译时): 未指定大小的、数学意义上的整数和实数类型,可能在字面量推断或comptime
计算的中间阶段使用,最终会“沉降”为具体的运行时类型。
类型推断与注解
Flurry 通常支持类型推断,允许在 let
绑定等地方省略类型注解,编译器会根据初始化表达式推断类型。但在函数签名、结构体字段等地方,通常需要显式类型注解。
let implicit_int = 42; -- 推断为 i32 (或默认整数类型)
let explicit_float: f64 = 3.14;
理解这些基本类型及其特征是构建任何 Flurry 程序的基础。它们提供了操作数据和表达计算所需的基本构件。
符号
符号 (Symbols)
符号 (Symbol) 是 Flurry 语言中一种独特的编译时常量类型,它提供了一种表示唯一标识符的轻量级方式。符号在 Flurry 的元编程、属性系统以及某些 DSL 构建场景中扮演着重要角色。
语法
符号字面量以一个点 .
开始,后跟一个合法的 Flurry 标识符:
.ok
.error
.pending
.my_custom_identifier
.color
.width
特性
- 唯一性 (Uniqueness): 在整个编译单元或特定上下文中(具体作用域规则待定),每个具有相同名称的符号字面量(例如
.ok
)都代表同一个唯一的符号值。 - 编译时常量 (Compile-time Constant): 符号是编译时的概念。它们的值在编译期间完全确定。这意味着它们可以自由地用于
comptime
计算、模式匹配、以及作为编译时object
的键。 - 轻量级 (Lightweight): 符号通常被实现为整数或指针大小的值,使得比较操作(判断两个符号是否相等)非常快速,通常只是一个简单的整数比较。
- 非字符串 (Not Strings): 尽管符号看起来像带前缀的标识符,但它们不是字符串。它们不直接支持字符串操作(如拼接、取子串)。它们的核心目的是作为唯一的原子标识符。可以通过取像操作
symbol'tag_name
来获取其对应的字符串表示(编译时常量字符串)。
用途
符号在 Flurry 中有多种重要用途:
-
枚举变体的简化表示 (Enum Variants): 虽然 Flurry 的层级化枚举使用
EnumName.Variant
形式,但在模式匹配或某些上下文中,可以直接使用符号来匹配简单的、无数据的枚举变体(如果语法支持这种简写)。-- 假设 Status 是 enum { ok, error, pending } match current_status { .ok => println("Status is OK"), -- 使用符号匹配 .error => handle_error(), .pending => wait_more(), }
(注意:这种用法是否可行取决于 Flurry 模式匹配的具体规则。)
-
编译时对象/映射的键 (Keys in Compile-time Objects/Maps): 正如在
comptime object
中看到的,符号是其属性键的标准形式。comptime let config = { .host "localhost", .port 8080, .log_level .debug -- 使用符号作为值 } let level = config.get(.log_level, Symbol)?; -- 使用符号作为键
-
属性系统的基础 (Foundation of Attribute System): Flurry 的属性系统(无论是内部定义
.attr value
还是外部应用^attr term
)都依赖符号来标识属性的名称。.packed
,.no_mangle
,.route
等都是符号。 -
轻量级标签/状态表示 (Lightweight Tags/State Representation): 在需要表示一组固定、互斥的状态或标签,并且不需要携带额外数据时,符号提供了一种比字符串或完整枚举更轻量级的方式。
-
元编程与反射 (Metaprogramming & Reflection): 在编译时操作类型信息时,字段名、方法名等可能被表示为符号。
与其他语言的比较
- Ruby/Elixir Symbols/Atoms: Flurry 的符号与 Ruby 的 Symbol (
:symbol
) 和 Elixir 的 Atom (:atom
) 非常相似,都用作轻量级、唯一的标识符常量。 - Julia Symbols: Julia 也有类似的
Symbol
类型 (:symbol
)。 - Lisp Symbols: Lisp 中的符号功能更强大,既是标识符也是数据,但 Flurry 的符号似乎更侧重于作为编译时常量标识符。
总结
符号是 Flurry 编译时系统中的一个重要构件。通过提供一种轻量级、唯一且编译时确定的标识符表示,符号简化了元编程、属性访问和某些模式匹配场景,并与 Flurry 的 comptime object
和属性系统紧密集成。理解符号的概念对于深入利用 Flurry 的编译时能力和编写惯用的 Flurry 代码非常重要。
第 4 章:变量、所有权与资源管理
Flurry 作为一门系统级编程语言,对资源的精确控制和内存安全给予了高度重视。与传统的 C/C++ 手动管理内存不同,也与纯粹的垃圾回收机制不同,Flurry 采用了一套结合了所有权 (Ownership)、仿射类型 (Affine Types) 和自动资源管理的机制,并辅以独特的可达性分析与副作用系统来确保安全性和效率。
本章将深入探讨 Flurry 如何管理变量的生命周期、数据的所有权以及相关资源的释放。理解这些核心概念对于编写安全、高效且无内存泄漏的 Flurry 代码至关重要。
核心概念:
- 变量绑定: 如何在 Flurry 中声明变量和常量。
- 所有权: 每个值在 Flurry 中都有一个明确的“所有者”。所有权决定了谁负责清理资源。
- 移动语义: 对于非
Copy
类型,所有权如何在变量绑定、函数调用和返回时发生转移。 - 仿射类型: 非
Copy
类型的值默认遵循“至多使用一次”的规则,这是确保资源安全的关键。 - 引用: 如何在不转移所有权的情况下安全地访问数据。
- 自动资源管理 (
drop
): 编译器如何自动管理资源的释放(如内存、文件句柄等)。 - 安全保证机制: 概述 Flurry 如何通过类型系统、可达性分析和副作用跟踪来保证内存安全(特别是引用的有效性)。
章节内容:
- 声明与绑定: 介绍
let
和const
如何用于创建变量和常量绑定,并讨论 Flurry 在可变性方面的设计选择。 - 仿射类型与移动语义: 详细解释
Once
,Clone
,Copy
Trait 层次,以及非Copy
类型的所有权转移规则。 - 引用与借用: 探讨指针 (
*
) 和切片 (Slice
) 等引用类型的作用,以及 Flurry 特色的后缀引用 (.ref
) 和解引用 (.*
) 语法。 - 资源管理与 Drop: 阐述 Flurry 的 RAII 原则和编译器自动插入
drop
调用的机制。 - 引用有效性:可达性与副作用: 简要介绍 Flurry 用于保证引用有效性的高级机制的目标和作用(为后续深入探讨做铺垫)。
掌握 Flurry 的所有权和资源管理系统是编写健壮系统级代码的基础。它旨在提供 C/C++ 级别的控制力和性能,同时具备现代语言的内存安全保证。
声明与绑定
在 Flurry 中,我们将值与名称相关联的过程称为绑定 (Binding)。通过绑定,我们可以方便地引用和操作数据。Flurry 提供了两种主要的绑定方式:let
用于变量绑定,const
用于常量绑定。
变量绑定 (let
)
使用 let
关键字可以创建一个变量绑定。
let message = "Hello, Flurry!";
let count = 0;
let pi: f64 = 3.14159; -- 显式类型注解
- 类型推断: Flurry 通常支持类型推断。如果初始化表达式的类型可以明确推断出来,你可以省略类型注解(如
message
和count
)。 - 显式注解: 你也可以使用
:
后跟类型来显式指定变量的类型(如pi
)。在函数签名、结构体字段等需要明确类型的地方,类型注解通常是必需的。
可变性 (Mutability):
与某些强制区分可变与不可变绑定的语言(如 Rust 的 let
vs let
)不同,Flurry(类似于 Zig 的设计思路)在 let
绑定层面可能不严格强制区分可变性。这意味着通过 let
绑定的变量默认可能是可变的。
let counter = 0;
counter = counter + 1; -- 假设 let 默认允许修改
-- 如果需要强制不可变绑定,语言可能有其他机制,
-- 或者依赖于后续的使用方式分析(例如,传递不可变引用)。
-- 目前我们假设 let 绑定允许后续赋值。
TODO: 明确 Flurry let
的确切可变性语义。是默认可变,还是有单独的 var
关键字,或者通过其他方式控制?当前暂按“默认可变”或“不强制区分”处理。
这种设计选择旨在简化变量声明,将可变性控制的重心更多地放在类型系统(例如,可变引用 *mut T
vs. 不可变引用 *T
)和副作用分析上。
常量绑定 (const
)
使用 const
关键字可以创建一个常量绑定。常量与变量的关键区别在于:
- 编译时求值:
const
绑定的值必须在编译时就能完全确定。初始化表达式必须是一个编译时常量表达式。 - 完全不可变: 常量的值在程序运行期间不能被改变。它们通常会被编译器直接内联或放入只读内存段。
- 类型注解通常需要: 常量的类型通常需要显式注解,因为编译器不会像对
let
那样进行复杂的类型推断(尽管简单的字面量类型可能可以推断)。
const MAX_USERS: usize = 1000;
const DEFAULT_TIMEOUT: Duration = Duration.seconds(5); -- 假设 Duration.seconds 是 comptime 函数
const APP_NAME: str = "Flurry Core System"; -- 编译时字符串
-- const RUNTIME_VALUE: i32 = get_runtime_input(); -- 非法,初始化器不是编译时常量
常量非常适合用于定义程序中不变的配置值、标志、或者需要在编译时进行计算和使用的值。它们是 Flurry 强大的 comptime
能力的基础体现之一。
作用域与遮蔽 (Scope & Shadowing)
Flurry 中的绑定遵循词法作用域规则。一个绑定只在其声明的代码块(及其子块)内有效。
fn main() {
let x = 10;
{ -- 进入新的作用域
let y = 20;
println("Inner: x = {i32}, y = {i32}", x, y); -- x 和 y 都可见
let x = 5; -- 在内层作用域 "遮蔽" 外层的 x
println("Inner (shadowed): x = {i32}", x); -- 输出 5
} -- y 在此离开作用域
-- println("Outer: y = {}", y); -- 错误,y 不在此作用域
println("Outer: x = {i32}", x); -- 输出 10,内层的 x 遮蔽结束
}
Flurry 允许在内部作用域使用 let
重新声明一个同名变量,这会遮蔽 (Shadow) 外部作用域的同名变量。被遮蔽的变量在内部作用域内暂时不可访问,直到内部作用域结束。遮蔽是一个有用的特性,例如用于类型转换或值的重新绑定,但过度使用可能会降低代码清晰度。
理解 let
和 const
的区别以及作用域规则,是管理程序状态和数据生命周期的基础。
仿射类型与移动语义
Flurry 的核心安全策略之一是其对资源所有权的管理。它借鉴并发展了仿射类型系统 (Affine Type System) 的思想,规定了值的“使用次数”,并以此为基础实现了移动语义 (Move Semantics),从而在没有传统垃圾回收器的情况下自动管理资源(如内存、文件句柄等)的生命周期。
Once
, Clone
, Copy
: 类型能力层次
在 Flurry 中,所有类型都隐式地属于一个能力层次,决定了它们的值可以如何被使用和复制:
-
Once
Trait (基础能力):- 所有类型都自动具备
Once
能力。这从概念上标记了一个值可以被销毁或最终使用一次。它更像是一个逻辑基础,表明每个值都有其生命周期的终点。对于实际编程而言,更重要的是Copy
和Clone
的缺失或存在。
- 所有类型都自动具备
-
Clone
Trait (可克隆):- 如果一个类型需要能够创建其值的深拷贝 (Deep Copy)(即创建一个完全独立的、拥有自己资源的新副本),它可以实现
Clone
Trait。 - 实现
Clone
通常需要显式地编写.clone()
方法来定义克隆逻辑。 Clone
的实例可以被多次使用——通过显式调用.clone()
来创建新的所有权实例。
- 如果一个类型需要能够创建其值的深拷贝 (Deep Copy)(即创建一个完全独立的、拥有自己资源的新副本),它可以实现
-
Copy
Trait (可按位复制):Copy
是Clone
的一个特殊子集。如果一个类型的克隆操作仅仅是简单的按位复制 (Bitwise Copy),并且复制后原始值仍然有效(即类型不拥有需要特殊释放的资源,如堆内存指针、文件句柄),那么它可以实现Copy
Trait。- 常见
Copy
类型: 包括基本类型(如整数i32
, 浮点数f64
, 布尔bool
, 字符char
)、只包含Copy
类型字段的结构体或元组,以及某些引用类型(如*T
,指针本身的复制是按位复制,但不复制指向的数据)。 - 隐式复制:
Copy
类型的值在赋值、函数参数传递(按值)或函数返回时,会自动进行按位复制。原始值和新副本都是有效的、独立的值。 - 无需实现
Clone
: 实现了Copy
的类型通常不再需要手动实现Clone
,因为简单的位复制就是其克隆方式(编译器可能会自动提供)。
移动语义 (Move Semantics)
核心规则: 对于没有实现 Copy
Trait 的类型(通常称为“移动类型”或“非 Copy
类型”),当它们的值被用在所有权转移的场景时,所有权会从源“移动”到目标,源将不再有效。
所有权转移场景:
-
赋值 (
let new_owner = old_owner;
):let s1 = String.from("hello"); -- String 通常不是 Copy 类型 let s2 = s1; -- s1 的所有权移动到 s2 -- println(s1); -- 编译错误!s1 不再拥有值,其绑定失效 println(s2); -- OK
-
函数参数传递 (按值):
fn takes_ownership(some_string: String) { println(some_string); } -- some_string 在这里离开作用域,其拥有的资源被 drop let message = String.from("world"); takes_ownership(message); -- message 的所有权移动到函数参数 some_string -- println(message); -- 编译错误!message 不再有效
-
函数返回值:
fn creates_string() -> String { let s = String.from("new string"); s -- 返回 s,所有权从 s 移动到函数调用者 } let my_string = creates_string(); -- my_string 接收了函数返回的 String 的所有权 println(my_string); -- OK
仿射类型:“至多使用一次”
移动语义是仿射类型系统规则的直接体现:一个非 Copy
的值,其所有权在任意时刻只能存在于一个地方。一旦所有权转移(被“移动”),原来的绑定就不能再被用来访问这个值了。你可以认为这个值被“消耗”了。
为什么需要移动语义/仿射类型?
- 资源安全: 这是 Flurry 自动管理资源的关键。对于持有堆内存、文件句柄、网络连接等资源的值,移动语义确保了只有一个所有者负责在适当的时候释放这些资源(通常通过
drop
机制)。它从根本上防止了二次释放 (Double Free) 错误。 - 数据竞争预防 (基础): 虽然完整的线程安全还需要其他机制,但所有权转移有助于防止多个线程同时持有对同一份可变数据(非
Copy
类型通常是可变的或包含可变部分)的写入权限。 - 性能: 避免了不必要的深拷贝。对于大型数据结构,移动通常比克隆高效得多(通常只是栈上指针或元数据的复制)。
总结:
Flurry 通过 Copy
Trait 区分了可以廉价、安全地进行按位复制的值和那些拥有独特资源所有权的值。对于后者(非 Copy
类型),Flurry 强制实行移动语义,确保资源所有权的唯一性,这是其内存安全和自动资源管理策略的基础。理解值是 Copy
还是 Move(非 Copy
)对于预测代码行为和避免所有权相关的编译错误至关重要。
引用与“借用”
Flurry 的所有权系统确保了每个值都有一个唯一的所有者,这对于资源管理至关重要。然而,在实际编程中,我们经常需要在不转移所有权的情况下访问或使用数据。例如,我们可能想让多个函数读取同一个配置对象,或者将一个大数据结构的片段传递给一个处理函数。为了满足这些需求,Flurry 提供了引用 (References)。
与所有权直接控制值本身不同,引用允许我们创建一个指向值的间接访问途径。在 Flurry 中,主要的引用类型是指针 (*T
) 和切片 (Slice<T>
)。
指针 (*T
)
指针是内存地址的直接表示,它“指向”内存中某个特定类型 T
的值所在的位置。
- 类型:
*T
表示一个指向类型T
的值的指针。Flurry 不在类型层面显式区分可变与不可变指针(即没有类似 Rust 的&T
vs&mut T
)。一个*T
是否允许修改其指向的数据,可能取决于指针的来源、上下文(如函数参数是否允许修改)或是否在unsafe
块内。(TODO: 明确指针的可变性规则和保证机制) - 获取指针: 可以使用后缀
.ref
操作符来获取一个值的指针。let data = MyData { ... } let data_ptr: *MyData = data.ref; -- 获取 data 的指针
- 解引用: 可以使用后缀
.*
操作符来访问指针指向的值。
TODO: 明确指针访问成员和调用方法的具体语法和解引用规则。let value_copy = data_ptr.*; -- 解引用,获取 data 的一个副本(如果 MyData 是 Copy) -- 或者可能获取对 data 内部字段的访问权 -- (解引用的具体语义,特别是对于非 Copy 类型,需要进一步明确) -- 访问字段或调用方法通常通过指针自动解引用(如果语言支持)或显式解引用 -- println(data_ptr.some_field); -- 类似 C/Zig 的隐式解引用? -- println((data_ptr.*).some_field); -- 或者需要显式解引用? -- data_ptr.some_method(); -- 方法调用是否自动解引用?
- 安全性: 直接操作原始指针(进行算术运算、类型转换等)通常被认为是
unsafe
操作。然而,Flurry 的目标是利用其可达性类型系统和副作用系统来保证即使是传递和使用指针(由.ref
创建,并在safe
代码中传递)也是安全的,主要是防止悬垂指针(即指针指向的内存不再有效或已被释放)。
切片 (Slice<T>
)
切片提供了一种指向内存中连续序列(如数组、向量或字符串的一部分)的视图。切片本身通常不拥有数据,而是“借用”了底层数据的一部分。
- 类型:
Slice<T>
表示一个包含T
类型元素的连续序列的引用。 - 构成: 一个切片通常包含两部分信息:一个指向序列起始元素的指针,以及序列的长度。
- 创建: 可以从数组、向量或其他支持切片操作的数据结构创建切片。
TODO: 确认切片创建的具体语法。let array = [1, 2, 3, 4, 5]; let full_slice: Slice<i32> = array.slice(..); -- 获取整个数组的切片 let partial_slice = array.slice(1..4); -- 获取索引 1 到 3 (不含 4) 的切片
- 用途: 切片非常适合用于处理数据缓冲区、字符串视图等,它允许函数操作数据的子集而无需复制或转移整个数据结构的所有权。
- 安全性: 与指针类似,切片的有效性(确保它指向的底层数据在切片存续期间保持有效)也依赖于 Flurry 的可达性类型系统和副作用系统的保证。
"借用" 的概念
虽然 Flurry 可能没有像 Rust 那样严格且形式化的“借用检查器 (Borrow Checker)”术语和规则集,但引用的核心目的仍然是实现临时、非拥有式的数据访问,这在概念上就是一种**“借用”**。
- 不转移所有权: 当你通过
.ref
获取指针或创建切片时,原始数据的所有权不会发生转移。原始所有者仍然负责数据的生命周期和最终的清理。 - 生命周期依赖: 引用的有效性依赖于其指向的数据的生命周期。引用不能比它所指向的数据活得更长。这是 Flurry 的可达性分析和副作用系统需要强制执行的关键保证。
示例对比:
fn consume_data(data: MyData) { -- 接受所有权
-- data 在此被消耗或在函数结束时 drop
}
fn use_data_ref(data_ref: *MyData) { -- "借用" 数据
-- 通过 data_ref 访问数据,但不拥有它
let field_value = data_ref.*.some_field; -- 假设需要显式解引用
-- ...
} -- data_ref 指针本身离开作用域,但不影响原始 data
fn main() {
let my_data = MyData { ... }
let data_ptr = my_data.ref; -- 创建引用(借用)
use_data_ref(data_ptr); -- 传递引用
use_data_ref(my_data.ref); -- 再次创建并传递引用 (OK)
consume_data(my_data); -- 转移所有权
-- use_data_ref(data_ptr); -- 编译错误!data_ptr 现在是悬垂指针,指向的数据已被移动
-- (Flurry 的安全系统需要能捕捉到这个)
}
总结
引用(指针和切片)是 Flurry 中实现数据共享和非拥有式访问的关键机制。通过 .ref
获取指针,通过切片语法获取序列视图,可以在不干扰所有权的情况下传递和使用数据。Flurry 不依赖显式的 mut
区分可变/不可变引用类型,而是计划通过其独特的可达性类型系统和副作用系统来保证引用的有效性(防止悬垂引用),从而在提供灵活性的同时确保内存安全。理解引用的“借用”本质及其与所有权的区别,对于编写正确的 Flurry 程序至关重要。
资源管理与 Drop 精化
Flurry 的所有权系统不仅仅是为了防止数据竞争或非法访问,其核心目标之一是实现可靠且自动的资源管理。无论是堆上分配的内存、打开的文件句柄、网络套接字还是其他系统资源,都需要在不再使用时被精确地释放,以避免泄漏。Flurry 通过类似于 C++ RAII (Resource Acquisition Is Initialization) 和 Rust Drop
Trait 的机制,结合编译器的Drop 精化 (Drop Elaboration) 来自动化这个过程。
RAII: 资源生命周期绑定到对象生命周期
Flurry 遵循 RAII 的核心思想:资源的生命周期与拥有该资源的对象(值)的生命周期相绑定。
- 获取即初始化: 当创建一个需要管理资源的对象时(例如,调用
File.open(...)
或Vec.new()
),该对象在其内部获取并管理所需的资源(文件句柄、堆内存)。 - 析构即释放: 当该对象离开其作用域或者其所有权结束时,它所拥有的资源必须被释放。
drop
操作与 Drop
Trait (假设)
为了实现资源的自动释放,Flurry 预计会提供一个类似 Rust Drop
Trait 的机制(具体名称和形式待定,我们暂且称之为 Drop
):
Drop
Trait: 需要自定义资源清理逻辑的类型可以实现Drop
Trait。这个 Trait 通常包含一个drop
方法。-- 概念性示例 trait Drop { fn drop(*self); -- drop 方法接收一个可变指针 } impl Drop for MyFileWrapper { fn drop(*self) { -- 关闭文件句柄,释放相关资源 unsafe { close_file_handle(self.handle) } println("MyFileWrapper dropped!"); } }
- 默认行为: 对于没有实现
Drop
Trait 的类型(例如只包含Copy
类型字段的结构体),其“drop”操作通常是无操作 (no-op),或者仅仅是递归地 drop 其包含的字段(如果字段本身实现了Drop
)。
编译器的 Drop 精化
开发者通常不需要手动调用 drop
方法。Flurry 编译器的关键职责之一就是执行 Drop 精化:在编译期间,静态地分析代码,并在每个值的所有权结束时自动插入对 drop
的调用。
编译器插入 drop
的时机:
编译器通过所有权和生命周期分析来确定何时插入 drop
:
-
离开作用域: 当一个拥有资源的值(非
Copy
类型)绑定的变量离开其声明的作用域时,并且其所有权没有被移动走,编译器会在此作用域结束处插入drop
调用。{ let file = File.open("temp.txt")!; -- file 拥有文件句柄 -- ... 使用 file ... } -- file 离开作用域,编译器在此处隐式插入 drop(file)
-
所有权转移后: 如果一个值的所有权被移动(例如,赋值给新变量、作为函数参数传递、从函数返回),则原来的绑定就不再拥有该值,编译器不会在其离开作用域时为其插入
drop
。drop
的责任随着所有权一起转移给了新的所有者。fn process_file(f: File) { -- ... f 在函数内部被使用 ... } -- f 在函数结束时离开作用域,编译器在此处插入 drop(f) let my_file = File.open("data.log")!; process_file(my_file); -- 所有权移动到 f -- my_file 离开作用域,但所有权已转移,编译器 *不* 在此处为 my_file 插入 drop
-
控制流: 编译器需要分析
if
/else
、match
、循环等控制流。如果一个值在某个代码分支中被移动或消耗,而在另一个分支中没有,编译器需要确保在后者对应的路径结束时插入drop
。fn example(use_it: bool) { let data = allocate_resource(); if use_it { consume_resource(data); -- data 所有权在这里被转移 } else { -- data 在这个分支没有被消耗 println("Resource not consumed."); } -- 编译器需要分析: -- 如果 use_it 为 true,data 已被移动,这里什么都不做。 -- 如果 use_it 为 false,data 仍然有效且即将离开作用域, -- 编译器会在此处(概念上是 else 分支结束和函数返回之间)插入 drop(data)。 }
这与您在问题描述中给出的
main
和consumes
函数示例的行为一致。
优势:
- 自动化: 开发者无需手动管理资源释放,减少了忘记释放资源导致泄漏的可能性。
- 及时性: 资源在不再需要时(所有权结束时)立即被释放,而不是等待垃圾回收器运行,这对于需要精确控制生命周期的资源(如文件锁、网络连接)非常重要。
- 确定性: 资源释放的时机是确定的(由作用域和所有权规则决定),便于推理程序的行为。
- 异常安全 (潜力): 如果与错误处理(例如
!
)或效应系统(如代数效应)结合,可以设计出即使在发生错误或非本地控制流转移时也能保证资源被正确释放的模式(例如,通过 unwind 或者特定的恢复机制调用drop
)。
总结:
Flurry 通过结合 RAII 原则、类似 Drop
Trait 的机制以及编译器的Drop 精化,实现了自动化、确定性的资源管理。编译器静态地跟踪值的所有权和生命周期,在所有权结束时自动插入资源释放逻辑 (drop
)。这使得开发者能够专注于业务逻辑,同时获得强大的内存安全和资源安全保证,避免了手动资源管理带来的许多常见错误。理解 Drop 精化是理解 Flurry 如何确保资源安全的关键。
引用有效性:可达性与副作用
第 5 章:表达式与运算符
表达式 (Expressions) 是 Flurry 程序中计算值的基本方式。它们由字面量、变量、常量、运算符和函数调用等组合而成。Flurry 的表达式系统设计旨在提供强大的表达能力,同时通过一致的语法风格(特别是后缀操作)提升代码的可读性和流畅性。
本章将深入探讨 Flurry 中各种表达式的构成、运算符的行为与优先级,以及 Flurry 独特的“取像”操作。
核心概念:
- 表达式求值: 任何表达式最终都会计算出一个值(除非它发散或产生副作用)。
- 运算符: 用于组合或修改值的特殊符号(如
+
,-
,>
,.
)。 - 优先级与结合性: 决定复杂表达式中运算符的计算顺序。Flurry 采用 Pratt 解析器常用的绑定力 (Binding Power) 或优先级方案来处理。
- 后缀风格: Flurry 大量采用后缀运算符和操作(如
.field
,.method()
,.*
,.ref
,'image
),使得链式调用非常自然。 - 无内置位运算符: Flurry 选择不提供传统的位运算符(
&
,|
,^
,~
,<<
,>>
),而是依赖bitvec
抽象视图或标准库函数(如math
包)进行位操作。 - 编译时连接 (
++
):++
运算符专用于编译时连接集合(如str
,meta.List
)。
章节内容:
- 常用运算符: 介绍 Flurry 支持的算术、比较、逻辑等运算符,并重点讨论它们的空格敏感性规则。
- 后缀风格与链式调用: 详细阐述选择运算符 (
.
)、解引用 (.*
)、取引用 (.ref
) 以及其他后缀操作如何促进流畅的链式调用。 - 取像操作 (expr ' image): 深入讲解
'
操作符的用途,以及如何获取表达式的不同“像”。
理解 Flurry 的表达式求值规则和运算符行为是编写任何非平凡程序的基础。其独特的后缀风格和对位运算的处理方式是需要特别注意的设计点。
常用运算符
运算符是 Flurry 语言中用于执行计算、比较、逻辑组合等操作的特殊符号。Flurry 提供了一组旨在清晰且富有表现力的运算符集合。
算术运算符
用于执行基本的数学运算。这些运算符通常可以被用户定义的类型通过实现特定的 Trait 来重载。
- 加法 (
+
):a + b
- 减法 (
-
):a - b
- 乘法 (
*
):a * b
- 除法 (
/
):a / b
(整数除法通常向零截断,浮点数除法遵循 IEEE 754) - 取模 (
%
):a % b
(计算除法的余数)
空格敏感性: 如词法结构中所述,这些二元算术运算符必须在其两侧使用空格,以便被词法分析器正确识别为标准算术运算符 token(例如 _+_
)。a+b
不会被识别为标准的加法运算。
let sum = count + 1;
let difference = total - tax;
let area = width * height;
let quotient = dividend / divisor;
let remainder = value % modulus;
比较运算符
用于比较两个值,结果通常是一个布尔值 (bool
)。它们也可以被重载。
- 等于 (
==
):a == b
- 不等于 (
!=
):a != b
- 小于 (
<
):a < b
- 小于等于 (
<=
):a <= b
- 大于 (
>
):a > b
- 大于等于 (
>=
):a >= b
空格敏感性: 小于 (<
) 和大于 (>
) 运算符同样具有空格敏感性。当用于比较时,它们两侧必须有空格(对应 _<_
和 _>_
token)。这避免了与泛型参数列表等场景中使用的尖括号 (<
, >
) 产生词法歧义。
let is_equal = response_code == 200;
let is_not_empty = size != 0;
let in_range = value >= min_value and value <= max_value; -- 注意 'and' 关键字
let needs_update = current_version < latest_version;
逻辑运算符
用于组合布尔值。Flurry 使用关键字而非符号来表示逻辑与和或。
- 逻辑与 (
and
):a and b
(短路求值:如果a
为false
,则不评估b
) - 逻辑或 (
or
):a or b
(短路求值:如果a
为true
,则不评估b
) - 逻辑非 (
not
):not a
(一元运算符)
let both_conditions_met = is_ready and has_permission;
let either_option_valid = is_primary or is_fallback;
let is_invalid = not is_valid;
赋值运算符
用于将值赋给变量绑定。
- 简单赋值 (
=
):variable = value
- 复合赋值:
+=
,-=
,*=
,/=
,%=
(例如a += b
等价于a = a + b
)
let score = 0;
score += 10; -- score 现在是 10
注意:由于 Flurry 可能默认绑定是可变的,let
声明的 score
可以被复合赋值操作修改。
其他重要操作符/结构
Flurry 还包含一些具有特殊语法和用途的操作符或结构,其中许多采用后缀形式(详见下一节):
- 选择运算符 (
.
): 用于访问成员、调用方法、限定路径等。 - 解引用 (
.*
): 后缀操作符,用于访问指针指向的值。 - 取引用 (
.ref
): 后缀操作符,用于获取一个值的指针。 - 取像 (
'
): 后缀操作符,用于expr ' image
操作。 - 调用操作符:
()
(函数调用),[]
(索引调用),{}
(expr object
调用/记录调用),<>
(泛型/钻石调用)。 - 错误/可选处理:
!
(错误传播/处理),?
(可选类型处理)。 - 效应处理:
#
(效应处理块)。 - 属性标记 (
^
): 用于附加属性。 - 管道操作符 (
|
,|>
): 用于函数式风格的数据流处理。
编译时连接运算符 (++
)
++
: 这个运算符专用于编译时,用于连接两个集合类的值。- 适用类型: 其具体行为取决于操作数类型所实现的特定 Trait(可重载)。常见的应用包括:
- 连接编译时字符串 (
str
)。 - 连接编译时列表 (
meta.List
)。 - 连接编译时集合 (
meta.Set
)。 - 连接
bitvec
抽象视图(编译器会将其优化为位操作)。
- 连接编译时字符串 (
- 限制: 不能直接用于运行时的值(除了
bitvec
抽象视图映射这种特殊情况,其本质也是编译时指导的优化)。
comptime {
const part1 = "Hello, ";
const part2 = "Flurry!";
const message = part1 ++ part2; -- message == "Hello, Flurry!"
const list1 = meta.List.new(1, 2);
const list2 = meta.List.new(3, 4);
const combined_list = list1 ++ list2; -- combined_list == [1, 2, 3, 4]
-- let runtime_vec1 = Vec.new();
-- let runtime_vec2 = Vec.new();
-- let result = runtime_vec1 ++ runtime_vec2; -- 编译错误!++ 不能用于运行时 Vec
}
关于位运算符
值得再次强调,Flurry 不提供内建的按位运算符(如 C/Java/Rust 中的 &
, |
, ^
, ~
, <<
, >>
)。进行位级别的操作需要:
- 使用
bitvec
抽象视图: 通过取像操作value'bitvec
获取值的位向量表示,然后对其进行连接 (++
) 或其他位操作(这些操作会被编译器优化)。 - 使用标准库函数: Flurry 计划提供一个内置的
math
(或其他)包,其中包含执行位移、按位与/或/异或等操作的函数(例如math.bit_and(a, b)
)。
这种设计选择旨在减少操作符集合的歧义,并将底层位操作封装在更明确的抽象(bitvec
)或库函数中。
运算符优先级与结合性
Flurry 使用 Pratt 解析器常用的绑定力 (Binding Power) 或等效的优先级方案来确定复杂表达式中运算符的计算顺序。您提供的 OpTable
Scala 代码片段清晰地展示了不同 token 对应的优先级数值(数值越高,通常绑定得越紧密)。
例如,根据该表:
- 乘法/除法/取模/
++
(70
) 的优先级高于加法/减法 (60
)。 - 加法/减法 (
60
) 的优先级高于比较运算符 (40
)。 - 比较运算符 (
40
) 的优先级高于逻辑与 (30
) 和逻辑或 (20
)。 - 后缀操作符如选择 (
.
, 100)、取像 ('
, 100)、调用 (()
,[]
,{}
,<>
, 90) 和可能的字面量拓展 (id
, 110) 具有非常高的优先级。
开发者通常不需要死记硬背完整的优先级表,而是遵循数学和逻辑运算的常规顺序,并在必要时使用圆括号 ()
来明确指定求值顺序。
let result = a + b * c; -- 等价于 a + (b * c),因为 * 优先级更高
let flag = x > 0 and y < 10; -- 等价于 (x > 0) and (y < 10)
let complex = (a + b) * (c - d); -- 使用括号强制优先级
后缀风格与链式调用
Flurry 语言在表达式语法设计上一个显著的特点是广泛采用后缀 (Suffix) 形式的操作符和语法结构。这种设计使得链式调用 (Method Chaining / Fluent Interface) 变得极其自然和流畅,提高了代码的可读性,使得代码逻辑能够从左到右平滑地展开。
核心的后缀操作符是选择运算符 (.
),但它的作用远不止访问字段。
选择运算符 (.
) 的多重角色
点号 .
是 Flurry 中最繁忙的操作符之一,它作为连接符,统一了多种常见的后缀操作:
-
访问结构体字段 (Field Access):
struct Point { x: i32, y: i32 } let p = Point { .x 10, .y 20 } let x_coord = p.x; -- 访问字段 x
-
调用方法 (Method Call):
struct Greeter { name: String } impl Greeter { fn greet(*self) { println("Hello, {str}!", self.name); } } let g = Greeter { .name "Flurry" } g.greet(); -- 调用 greet 方法
注意,方法调用本身
()
也是一种后缀操作,与.
结合使用。 -
访问层级化枚举变体 (Enum Variant Access):
enum Color { rgb.{ r: u8, g: u8, b: u8 }, hsv.{...} } let red = Color.rgb.{ .r 255, .g 0, .b 0 } -- 使用 . 选择层级
-
限定模块路径 (Module Path Qualification): 在
use
语句或直接引用时,用于分隔模块路径。use std.io.println; std.math.sqrt(4.0); -- 使用 . 限定路径
-
连接特殊后缀操作 (Connecting Special Suffix Operations): 这是 Flurry 后缀风格的关键体现。
.
用于连接表达式和语言内建的特殊后缀操作:- 解引用 (
.*
):let ptr = data.ref; let value = ptr.*; -- 使用 . 连接 *
- 取引用 (
.ref
):let ptr = data.ref; -- 使用 . 连接 ref 关键字
- 类型转换/断言 (
.as(TypeExpr)
):let any_value: Any = get_value(); let specific_value = any_value.as(MyType)?; -- 使用 . 连接 as(...)
- Trait 对象转换 (
.dyn(TraitExpr)
): (如果支持)let concrete_value = MyStruct {} let trait_object = concrete_value.dyn(MyTrait); -- 使用 . 连接 dyn(...)
- (可能还有其他类似
.some_op(...)
的内置操作)
- 解引用 (
链式调用的优势
这种统一的后缀风格使得链式调用非常自然:
let result = get_data_source() -- 1. 获取数据源
.ref -- 2. 获取其指针 (假设返回指针的方法)
.process_intermediate()? -- 3. 调用处理方法 (可能返回 Result/Optional)
.* -- 4. 解引用得到结果 (假设 process 返回指针)
.filter_items(|item| item.is_valid()) -- 5. 调用过滤方法
.map_values(|item| item.transform()) -- 6. 调用映射方法
.collect<Vec<_>>(); -- 7. 调用收集方法,注意泛型调用也是后缀 `<...>()`
-- 如果没有后缀风格,代码可能看起来像:
-- let source = get_data_source();
-- let ptr = &source; -- 或者 source.get_ref()
-- let intermediate_ptr = process_intermediate(ptr)?;
-- let intermediate_value = *intermediate_ptr; -- 或者 intermediate_ptr.deref()
-- let filtered = filter_items(intermediate_value, |item| item.is_valid());
-- ... 等等,更加嵌套和分散
链式调用使得数据处理流水线和连续操作的逻辑能够以线性的、从左到右的方式表达,更符合阅读习惯,减少了嵌套和临时变量。
其他后缀操作
除了通过 .
连接的操作外,Flurry 还有其他重要的后缀操作:
- 取像操作 (
expr ' image
): 获取表达式的某个“像”,如类型、标签名、位向量表示等。let type_info = my_variable'type; let tag_name = my_enum_value'tag_name;
- 调用操作符:
()
: 函数/方法调用。[]
: 索引访问/调用。{}
:expr object
调用 / 记录调用。<>
: 泛型参数传递 / 钻石调用。
- 错误/可选/效应处理:
!
,?
,#
通常也紧跟在表达式之后。 - 字面量拓展:
10px
,"s"c
可以视为对字面量表达式的后缀操作。 match
表达式 (后缀形式):expr match { ... }
(如果match
是后缀关键字)。
优先级
根据您提供的 OpTable
,这些后缀操作(.
, '
, ()
, []
, {}
, <>
) 具有非常高的优先级(90-100),确保它们会紧密地绑定到其左侧的操作数(表达式),这对于链式调用的正确解析至关重要。字面量拓展对应的 id
甚至有更高的优先级 (110),确保 10px
被视为一个整体而非 10
和 px
分开。
总结
Flurry 对后缀操作符和语法的广泛采用,特别是万能的选择运算符 (.
),是其语法设计的一大特色。它极大地促进了链式调用的流畅性和代码的线性可读性,并为多种不同的操作(字段访问、方法调用、内置操作如解引用/取引用/转换)提供了一致的语法入口。这种设计选择使得 Flurry 代码在处理连续操作和数据流时显得尤为优雅和简洁。
取像操作 (expr ' image)
Flurry 引入了一种独特且强大的后缀操作符——取像操作 ('
),用于获取一个表达式在不同抽象层面或元数据维度的“像 (Image)”。这个操作符提供了一个统一的语法入口,访问与表达式相关的编译时信息、运行时表示或其他关联属性。
语法
取像操作的基本语法形式为:
expression ' image_identifier
expression
: 任何有效的 Flurry 表达式。'
(单引号): 取像操作符。image_identifier
: 一个标识符,指定想要获取的“像”的类型。这个标识符不是变量或关键字,而是由 Flurry 编译器内部识别的、用于特定取像操作的名称。
根据您提供的优先级表 (OpTable
),取像操作符 '
具有很高的优先级 (100),确保它紧密绑定其左侧的 expression
。
概念与用途
“取像”可以理解为获取表达式 expression
的某个特定方面或表示:
-
元数据 (Metadata): 获取与表达式相关的元信息。
expression ' type
: 获取表达式在编译时确定的类型。结果本身是一个编译时Type
类型的值。let x = 10; comptime let type_of_x = x'type; -- type_of_x 的值是代表 i32 (或默认整数类型) 的 Type 对象
enum_value ' tag_name
: 获取枚举值当前变体的名称(作为一个编译时常量字符串)。enum Status { ok, loading, error } let current = Status.loading; comptime let name = current'tag_name; -- name 的值是 "loading"
-
抽象表示 (Abstract Representation): 获取表达式底层数据的一种抽象视图。
value ' bitvec
: 获取值value
在内存中的位向量 (Bit Vector) 表示。结果是一个编译时可知长度(基于value
的类型)的BitVec<N>
抽象类型,可以用于编译时的位操作(编译器会优化为高效的机器指令)。let flags: u8 = 0b1010_0101; comptime let bv = flags'bitvec; -- comptime let first_nybble = bv.slice(0..4); -- 假设 bitvec 支持切片
-
关联操作或转换 (Associated Operation / Transformation): 触发某种与表达式相关的特殊操作。
dst_pointer ' dst_deref
: (用于动态大小类型 DST) 这不是获取一个简单的值,而是触发一种特殊的解引用操作,允许对 DST 指针指向的内容进行模式匹配或访问,编译器会根据指针指向的实际数据(例如tagged_polymorphic
枚举的 tag)来确定如何解释内存。tuple ' enumerate
: (用于元组) 可能返回一个包含(元素, 索引)
对的迭代器或编译时可展开的序列,用于inline for
。
编译时与运行时
取像操作的结果可能是:
- 编译时常量: 如
'type
的结果是编译时Type
,'tag_name
的结果是编译时str
。这些结果可以直接用于comptime
代码块和编译时条件判断。 - 运行时值: 如
'bitvec
得到的是一个抽象表示,但最终的操作会生成运行时代码。'dst_deref
也是在运行时执行的特殊解引用。'enumerate
可能产生运行时迭代器。 - 操作触发器: 某些“像”可能不直接返回值,而是触发编译器执行特定的分析或代码生成过程。
编译器负责根据 image_identifier
确定具体的语义和结果类型。
内置的 "像"
Flurry 语言会预定义一系列有用的 image_identifier
。一些我们已经讨论过的或可能的例子包括:
'type
: 获取编译时类型。'tag_name
: 获取枚举变体名(字符串)。'bitvec
: 获取位向量抽象视图。'dst_deref
: 用于 DST 指针的特殊解引用/匹配。'enumerate
: 用于元组或其他序列的带索引迭代。- (可能还有
'size
获取类型大小,'alignment
获取对齐,'address
获取地址等)
这个列表是由语言定义和编译器内置的,开发者不能自定义新的 'image_identifier
。
设计优势
- 统一语法: 为多种不同的元信息查询和底层操作提供了一致的后缀语法。
- 简洁:
expr'type
比get_type(expr)
或typeof(expr)
更简洁。 - 与编译时系统集成: 许多“像”直接产生编译时值,无缝融入
comptime
计算。 - 表达力: 提供了访问语言底层信息和触发特殊操作的强大能力。
总结
取像操作 (expr ' image
) 是 Flurry 中一个富有创新性的特性。它通过一个简洁的后缀操作符 '
和编译器内置的 image_identifier
,提供了一个统一的接口来访问表达式的类型、元数据、抽象表示或触发特殊操作。这是 Flurry 强大的编译时元编程和底层控制能力的又一体现。
基本控制流 (Control Flow)
程序很少是完全线性的。根据不同的条件执行不同的代码路径,或者重复执行某段代码,是构建复杂逻辑的基础。Flurry 语言提供了结构化的控制流语句,用于引导程序的执行顺序。
本章将详细介绍 Flurry 中的主要控制流机制,包括:
- 条件语句: 根据布尔表达式的结果选择执行分支 (
if
,when
)。 - 循环语句: 重复执行代码块 (
for
,while
)。 - 控制转移语句: 改变正常的执行流程 (
break
,continue
,return
)。
条件语句 (Conditional Statements)
条件语句允许程序根据特定条件的值来选择性地执行代码块。Flurry 主要提供 if
和 when
两种基本条件语句。
if
语句
if
语句是最基础的条件控制结构。它评估一个布尔表达式,如果结果为 true
,则执行紧随其后的代码块;否则,可以选择性地执行 else
或 else if
分支。
基本语法:
if <condition_expr> {
-- Code to execute if condition_expr is true
} else if <another_condition_expr> {
-- Code to execute if another_condition_expr is true
} else {
-- Code to execute if all preceding conditions are false
}
<condition_expr>
: 必须是一个求值为布尔值 (bool
) 的表达式。{ ... }
: 花括号定义了与条件关联的代码块。else if
和else
子句是可选的。可以有零个或多个else if
子句,最多一个else
子句。
示例:
let temperature = 25;
if temperature > 30 {
println("It's hot!");
} else if temperature < 10 {
println("It's cold!");
} else {
println("Temperature is moderate.");
}
-- 输出: Temperature is moderate.
let is_active = false;
if is_active {
-- This block will not be executed
println("User is active.");
}
if
表达式: if
语句也可以作为表达式使用,其每个分支必须返回兼容类型的值。
let access_level = if user.is_admin {
"admin"
} else if user.is_moderator {
"moderator"
} else {
"guest"
}
-- access_level 的类型会被推断为 str (或 String)
条件守卫 (if_guard
): 在其他语句(如 return
, break
, continue
)后紧跟 if <condition_expr>
,可以在满足条件时才执行该语句。
fn find_first_even(numbers: Slice<i32>) -> ?i32 {
for number in numbers {
return number if number % 2 == 0; -- 如果 number 是偶数,则立即返回
}
return null; -- 没有找到偶数
}
这提供了一种简洁的方式来表达提前退出的逻辑。
when
语句
when
语句提供了一种更结构化的多分支选择方式,类似于其他语言中的 switch
或 match
语句,但其分支条件是任意布尔表达式。它依次评估每个分支的条件,并执行第一个条件为 true
的分支所对应的代码。
基本语法:
when {
<condition_expr_1> => <statement_or_block_1>,
<condition_expr_2> => <statement_or_block_2>,
-- ... more branches
-- 可选的 else 分支 (相当于 true => ...)
_ => <default_statement_or_block>,
}
=>
: 用于分隔条件和要执行的语句或代码块。when
语句会按顺序检查<condition_expr>
,一旦找到第一个为true
的条件,就执行其对应的代码,然后退出when
语句(不会继续检查后续分支)。- 可选的
_ => ...
分支相当于true => ...
,当前面所有条件都不满足时执行。如果没有_
且所有条件都为false
,则when
语句不执行任何分支。
示例:
let status_code = 404;
when {
status_code == 200 => println("OK"),
status_code == 404 => println("Not Found"),
status_code >= 500 => println("Server Error"),
else => println("Unknown Status: {}", status_code),
}
-- 输出: Not Found
when
表达式: 与 if
类似,when
也可以作为表达式使用,所有分支(如果存在)必须返回兼容类型的值。
let status_category = when {
status_code >= 200 and status_code < 300 => "Success",
status_code >= 400 and status_code < 500 => "Client Error",
status_code >= 500 => "Server Error",
else => "Other",
}
-- status_category 将是 "Client Error"
对比 if
/else if
与 when
:
if
/else if
链更通用,可以嵌套。when
对于多个并列条件的检查通常更清晰、更结构化,避免了深层嵌套的else if
。
选择哪种取决于具体的逻辑和代码风格偏好。
循环语句 (Looping Statements)
循环语句允许程序重复执行一段代码,直到满足特定条件为止。Flurry 提供了 for
和 while
两种主要的循环结构。
for
循环
for
循环主要用于迭代一个序列或集合中的元素。它可以遍历数组、切片、范围、迭代器或其他可迭代对象。
基本语法 (遍历迭代器):
for <pattern> in <iterable_expr> {
-- Loop body, executed for each item
}
<iterable_expr>
: 必须是一个实现了IntoIterator或Iterator的表达式。<pattern>
: 用于在每次迭代中绑定从迭代器获取的当前元素。可以使用简单的变量名,也可以使用更复杂的模式(如元组(index, value)
)。- 循环体
{ ... }
: 对迭代器产生的每个元素执行一次。当迭代器耗尽时,循环结束。
示例:
let numbers = [10, 20, 30];
-- 遍历数组元素
for num in numbers {
println("Number: {}", num);
}
-- 输出:
-- Number: 10
-- Number: 20
-- Number: 30
-- 遍历范围
for i in 0..3 { -- 遍历 0, 1, 2
println("Index: {}", i);
}
-- 遍历并获取索引
for (index, value) in numbers.into_iter().enumerate() {
println("Item at {}: {}", index, value);
}
循环标签 (Loop Labels): 可以为 for
循环(以及 while
循环)添加标签,用于在嵌套循环中精确控制 break
和 continue
的目标。标签以 :
结尾。
for:outer i in 0..3 {
for:inner j in 0..3 {
continue outer if i == 1 and j == 1; -- 跳过外层循环的剩余部分,开始下一次外层迭代
break outer if i == 2 and j == 0; -- 完全跳出外层循环
println("i={}, j={}", i, j);
}
}
while
循环
while
循环会在其条件表达式求值为 true
时重复执行代码块。
基本语法:
while <condition_expr> {
-- Loop body, executed as long as condition_expr is true
}
<condition_expr>
: 在每次循环迭代之前进行求值。如果为true
,则执行循环体;如果为false
,则退出循环。
示例:
let count = 0;
while count < 3 {
println("Count is: {}", count);
count += 1;
}
-- 输出:
-- Count is: 0
-- Count is: 1
-- Count is: 2
-- 无限循环 (通常与 break 结合使用)
while true {
let input = read_input()?; -- 假设 read_input 可能返回错误
break if input == "quit"; -- 退出无限循环
process(input);
}
while is
(模式匹配循环): Flurry 还提供了将模式匹配与 while
结合的循环方式,允许在每次迭代时对某个表达式的值进行模式匹配,并在匹配成功时继续循环。
基本语法:
while <expr> is <pattern> {
-- Loop body, executed if expr matches pattern
-- pattern 中绑定的变量在此可用
}
<expr>
: 每次循环前求值的表达式。<pattern>
: 用于匹配<expr>
结果的模式。- 只有当
<expr>
的值成功匹配<pattern>
时,循环体才会执行。模式中绑定的变量可以在循环体中使用。当匹配失败时,循环终止。
示例 (处理 Option):
let optional_value = 5;
while optional_value is some? { -- 只要 optional_value 是 some? 就继续
println("Processing value: {}", some);
-- 更新 optional_value 以便最终退出循环
optional_value = if some > 0 { some - 1 } else { null }
}
-- 输出:
-- Processing value: 5
-- Processing value: 4
-- Processing value: 3
-- Processing value: 2
-- Processing value: 1
-- Processing value: 0
println("Loop finished.");
这对于处理迭代器、队列或其他可能返回“哨兵值”(如 null
, EndOfFile
)来表示结束的数据源非常有用。
控制转移语句 (Control Transfer Statements)
控制转移语句允许程序非顺序地改变其执行流程,例如提前退出循环或函数。Flurry 提供了 break
, continue
, 和 return
三种主要的控制转移语句。
break
break
语句用于立即终止其所在的最内层循环(for
或 while
)的执行。程序控制流将跳转到该循环语句之后的下一条语句。
示例:
let numbers = [1, 5, -3, 8, 2];
let found_negative = false;
for num in numbers {
if num < 0 {
found_negative = true;
break; -- 找到第一个负数,立即退出 for 循环
}
println("Checking positive number: {}", num);
}
if found_negative {
println("Found a negative number.");
} else {
println("All numbers are non-negative.");
}
-- 输出:
-- Checking positive number: 1
-- Checking positive number: 5
-- Found a negative number.
带标签的 break
: 如果存在嵌套循环,可以使用标签来指定 break
要终止的是哪个循环。
for:outer i in 0..5 {
for j in 0..5 {
if i * j > 10 {
println("Found i*j > 10 at i={}, j={}. Breaking outer loop.", i, j);
break outer; -- 终止名为 'outer' 的循环
}
}
}
continue
continue
语句用于跳过当前循环迭代中剩余的代码,并立即开始下一次迭代。
- 在
while
循环中,控制流会跳转回循环条件的判断处。 - 在
for
循环中,控制流会跳转到下一次迭代(获取迭代器的下一个元素)。
示例:
for i in 0..5 {
if i % 2 == 0 {
continue; -- 如果 i 是偶数,跳过本次迭代的 println
}
println("Found odd number: {}", i);
}
-- 输出:
-- Found odd number: 1
-- Found odd number: 3
带标签的 continue
: 与 break
类似,可以使用标签来指定 continue
要作用于哪个嵌套循环。
for:outer i in 0..3 {
for j in 0..3 {
if i == 1 {
println("Skipping outer iteration for i=1");
continue outer; -- 跳过外层循环 i=1 的剩余部分,直接开始 i=2
}
println("Processing i={}, j={}", i, j);
}
}
return
return
语句用于从当前函数中退出,并可选地将一个值返回给函数的调用者。
语法:
return; -- 从返回 void 的函数退出
return <value_expr>; -- 从返回特定类型的函数退出,并返回值
<value_expr>
: 其类型必须与函数签名中声明的返回类型兼容。- 一旦执行
return
语句,函数立即终止,控制权交还给调用点。
示例:
fn check_age(age: u32) -> bool {
if age < 18 {
println("Too young.");
return false; -- 提前返回 false
}
println("Age is sufficient.");
return true; -- 返回 true
}
fn process() {
println("Processing start.");
if some_condition() {
return; -- 提前退出 process 函数 (假设 process 返回 void)
}
println("Processing continued.");
-- ...
println("Processing end.");
}
return
与 if_guard
: return
可以与条件守卫结合使用,形成简洁的提前返回模式。
fn get_user(id: Uuid) -> ?User {
let data = database.fetch(id)?; -- 假设 ? 用于错误传播或 Option 解包
return null if data.is_empty(); -- 如果数据为空,提前返回 null
-- ... process non-empty data ...
User.from(data)
}
总结: break
, continue
, 和 return
是控制程序非线性执行流程的基本工具。break
和 continue
主要用于控制循环,而 return
用于退出函数。标签的使用为嵌套结构提供了更精确的控制能力。
函数 (Functions)
函数是 Flurry 程序中执行特定任务的基本代码单元。它们封装了可重用的逻辑,接收输入参数,执行计算,并可能返回一个结果。Flurry 的函数系统设计灵活,支持多种参数类型、泛型、编译时计算以及与类型系统紧密集成的方法。
本章将全面介绍 Flurry 中的函数,包括:
- 函数定义与调用: 如何声明和使用函数。
- 参数详解: 不同种类的函数参数及其用法。
- 返回值: 函数如何返回值,以及集成的错误处理机制。
- 方法: 如何在类型(如
struct
,enum
)上定义函数。 - 泛型函数: 如何编写可处理多种类型的通用函数。
函数是组织和构建 Flurry 程序的核心构件。
定义与调用 (Definition & Invocation)
函数定义 (Function Definition)
使用 fn
关键字定义一个函数。其基本语法结构如下:
pub? (comptime|async|...)? fn function_name(parameter*) ReturnType? clause? block
示例:
-- 简单的加法函数
fn add(a: i32, b: i32) -> i32 {
a + b -- 函数体最后是表达式,隐式返回其值
}
-- 无返回值的函数
pub fn print_greeting(name: String) { -- 等同于 -> void
println("Hello, {}!", name);
}
-- print_value: for<T:- Display> fn(T)
fn print_value(value: T) where T:- Display {
println("Value: {}", value);
}
函数调用 (Function Invocation)
调用函数使用函数名,后跟圆括号 ()
,括号内提供实际参数(也称为实参,arguments)。
let sum = add(5, 3); -- 调用 add 函数,传递 5 和 3 作为参数
print_greeting("Flurry".to_string()); -- 调用 print_greeting
-- 调用泛型函数时,编译时参数可能需要显式提供或由编译器推断
-- print_value<i32>(10);
print_value(10); -- 编译器通常可以推断 T 为 i32
参数传递的规则取决于函数签名中参数的类型(详见 参数详解)。
方法 (Methods)
函数也可以直接关联到某个类型(如 struct
, enum
, union
, mod
)上。定义在类型内部的函数称为关联函数。
定义:
关联函数直接在类型定义的 {}
内部使用 fn
定义。
struct Point {
x: f64,
y: f64,
pub fn origin() -> Self { -- Self 是当前类型的别名
Point { .x 0.0, .y 0.0 }
}
-- 通常称为“方法”
pub fn distance_from_origin(*self) -> f64 {
-- self 指向调用该方法的实例
math.sqrt(self.x * self.x + self.y * self.y)
}
-- 接收可变引用的方法
pub fn translate(*self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
-- 接收所有权的方法
-- pub fn consume(self) { {- ... -} }
}
调用:
let p1 = Point.origin(); -- 调用静态方法 origin
let p2 = Point { .x 3.0, .y 4.0 }
let dist = p2.distance_from_origin(); -- 调用实例方法 distance_from_origin
println("Distance: {}", dist); -- 输出: Distance: 5.0
let p3 = Point.origin();
p3.translate(1.0, 2.0); -- 调用可变实例方法 translate
println("Translated: ({}, {})", p3.x, p3.y); -- 输出: Translated: (1.0, 2.0)
方法调用 instance.method(args...)
通常是 TypeName.method(instance.ref, args...)
等形式的语法糖。
实现块 (impl
) 中的方法:
除了直接在类型定义中,也可以在单独的 impl TypeName { ... }
或 impl TraitName for TypeName { ... }
块中为类型定义函数、子成员等。
struct Counter { value: i32 }
-- 在 impl 块中为 Counter 添加方法
impl Counter {
fn new() -> Self { Counter { .value 0 } }
fn increment(*self) { self.value += 1; }
fn get(*self) -> i32 { self.value }
}
test {
let c = Counter.new();
c.increment();
println("Count: {}", c.get()); -- 输出: Count: 1
}
这种方式有助于将类型的定义和其行为实现分开
参数详解 (Parameter Details)
函数通过参数接收输入数据。Flurry 提供了多种参数类型,以满足不同的编程需求和代码风格。
1. 普通参数 (Positional Parameters)
这是最基本的参数形式,通过位置进行传递。
定义: parameter_name: Type
fn process_data(data: Slice<u8>, length: usize) { {- ... -} }
调用: 按定义顺序提供值。
let buffer = [1, 2, 3];
process_data(buffer[..], buffer.len());
2. 可选参数 (Optional Parameters)
参数类型为option type
, 则可以选择性传递该参数,不传递则传递null
。
定义: parameter_name: ?Type
-- timeout 是一个可选的毫秒数
fn connect(address: String, timeout: ?u64) -> bool {
let resolved_timeout = timeout.unwrap_or(5000); -- 使用默认值 5000ms
-- ... 连接逻辑使用 resolved_timeout ...
true -- 假设连接成功
}
调用:
connect("example.com".to_string()); -- 不提供超时
connect("example.com".to_string(), 10000); -- 提供 10 秒超时
3. 具名参数 (Named Parameters)
用于提高函数调用的可读性,尤其是在参数较多或参数意义不明显时。
定义: .parameter_name: Type = default_value
- 必须以点
.
开头。 - 必须提供一个默认值。
-- 配置选项通常使用具名参数
fn configure_service(
.retries: usize = 3,
.use_tls: bool = true,
.log_level: LogLevel = LogLevel.info,
) { {- ... -} }
调用: 调用时必须使用参数名,顺序任意。可以省略使用默认值的参数。
-- 使用部分默认值
configure_service(.log_level = LogLevel.Debug);
-- 指定多个参数,顺序随意
configure_service(.use_tls = false, .retries = 5);
-- 使用所有默认值
configure_service();
-- configure_service(5, false); -- 错误!必须使用名称
4. 变长参数 (Variadic Parameters)
允许函数接受不定数量的参数。
定义: ...parameter_name: TupleType
(通常放在最后)
- 使用
...
前缀。 - 调用时传递给该部分的所有参数会被收集到一个元组 (tuple) 中。
TupleType
指定了期望的元组类型。
调用: 正常传递参数,它们会被自动收集。
-- 接受任意数量 i32 的函数
fn sum_all(...numbers: T) -> i32
where T, requires Type.is_tuple(T)
{
let total = 0;
inline for num in numbers {
inline if num'type != i32 {
build.error("All arguments must be i32");
} else {
total += num;
}
}
total
}
test {
println("Sum: {}", sum_all(1, 2, 3, 4)); -- numbers 是 (1, 2, 3, 4)
println("Sum: {}", sum_all(10)); -- numbers 是 (10,)
println("Sum: {}", sum_all()); -- numbers 是 ()
}
编译时类型安全的变长参数: 结合 comptime
参数,可以实现一些依赖类型特性。
-- format 在编译时确定,varargs 的类型也随之确定
fn println(comptime format: str, ...varargs: fmt.Args(format)) {
-- 内部使用 inline for 安全处理
-- ...
}
双变参调用: Flurry 支持一种特殊的双变参函数和调用语法,用于构建 DSL。(详见相关章节)
5. 隐式参数 (Implicit Parameters)
由编译器自动提供的参数,无需在调用点显式传递。
定义: implicit parameter_name: Type
-- 获取源代码位置
fn log(message: String, implicit __src__: builtin.SourceLocation) {
println("{}:{}: {}", src.file, src.line, message);
}
调用: 调用者像调用普通函数一样,编译器负责查找并传入合适的隐式参数。
-- let memory = allocate_memory(1024); -- 编译器自动传入默认分配器
log("Initialization complete."); -- 编译器自动传入调用点的源代码位置
隐式参数的解析规则(如何查找、优先级等)需要语言明确定义。它们是实现上下文传递、依赖注入等模式的有力工具。
6. 编译时参数 (comptime
)
参数值必须在编译时可知。
定义: comptime parameter_name: Type
- 可以与其他参数种类(普通、可选、具名、变长)结合。
- 值在编译时确定。
- 后续参数的类型或值可以依赖于前面的
comptime
参数(依赖类型)。
-- T 和 N 都是编译时参数
fn create_array(initial_value: T) -> Array<T, N>
where T:- Copy, N: usize -- 约束 T 必须是可复制的,N 必须是无符号整数
{
Array<T, N>.new(initial_value) -- 使用 T 和 N 创建数组
}
-- alignment 必须是编译时常量
fn aligned_alloc(size: usize, comptime alignment: usize) -> !AllocErr *u8 {
-- ... 使用 alignment 进行内存分配 ...
}
test {
let arr = create_array(0); -- T 是 i32,N 是 10
println("Array: {:?}", arr);
let ptr = aligned_alloc(1024, 16)!; -- alignment 是 16
println("Aligned pointer: {:?}", ptr);
}
comptime
参数是 Flurry 实现泛型、元编程和零成本抽象的关键。每次使用不同的编译时参数调用函数时,编译器会生成一个该函数特化(单态化)的版本。
返回值与错误处理 (!)
函数通过返回值将其计算结果或状态传递给调用者。Flurry 提供了标准的返回值机制,并内置了一套用于处理可恢复错误的、基于类型系统的机制。
返回值 (Return Values)
使用 -> ReturnType
在函数签名中指定返回值的类型。
- 隐式返回: 如果函数体的最后一个表达式的类型与
ReturnType
兼容,该表达式的值将自动作为返回值,无需return
关键字。fn double(x: i32) -> i32 { x * 2 -- 表达式 x * 2 的值被隐式返回 }
- 显式返回: 使用
return
关键字可以从函数体的任何位置提前返回一个值。fn find_char(s: String, target: char) -> ?usize { -- 返回 ?usize for (i, c) in s.chars().enumerate() { return i if c == target; -- 找到,提前返回 Some(index) } null -- 循环结束都没找到,返回 null }
void
返回类型: 如果函数不返回有意义的值,返回类型为void
。可以省略-> void
。fn print_message(msg: String) { -- 隐式返回 void println("{}", msg); }
错误处理 (!Errors T
)
Flurry 提供了一种内置的、类型安全的方式来处理函数中可能发生的可恢复错误,避免了 C 的错误码、Java 的检查异常或 Go 的显式 if err != nil
的某些缺点。
!Errors T
返回类型:
- 当一个函数可能成功并返回
T
类型的值,或者失败并返回多种可能的错误类型之一时,可以使用!Errors T
作为返回类型。 Errors
: 通常是一个编译时已知的类型列表(或单个枚举类型),代表该函数可能返回的所有错误类型。
语法:
-- 可能返回 u32,或 FileNotFoundErr, 或 PermissionDenied 错误
fn read_config_value(path: String) -> ![FileNotFoundErr, PermissionDenied] u32 {
-- ... 函数体 ...
}
错误传播 (expr!
):
当调用一个返回 !Errors T
类型的函数时,可以使用后缀操作符 !
简洁地传播错误。
- 如果
expr
的结果匹配value!
,expr!
会解包得到value
。 - 如果
expr
的结果匹配error e
,expr!
会立即从当前函数返回这个e
值。前提是当前函数的返回类型也必须是!Errors' T'
,并且Errors'
包含e'type
(或者可以自动转换/融合)。
struct Config { {- ... -} }
fn parse_config(content: String) -> !ParseErr Config { {- ... -} }
fn read_file(path: String) -> !IoErr String { {- ... -} }
pub const Errors = [IoErr, ParseErr, NetworkErr];
-- load_config 可能返回
fn load_config(path: String) -> !Errors Config {
-- read_file返回值的错误负载必须是Errors的子集或元素
let content = read_file(path)!;
let config = parse_config(content)!;
config
}
!
操作符极大地简化了错误传递的样板代码。
错误处理 (expr! { ... }
):
如果你想在错误发生的地方立即处理它,而不是直接传播,可以使用 expr! { ... }
块。
- 它允许你为
expr
可能返回的不同错误类型提供匹配分支。
fn get_data_or_default(source: DataSource) -> Data {
let result = source.fetch_data()! { -- 处理 fetch_data 可能的错误
.NetworkErr(err) => log("Network failed: {}", err),
.AuthErr => {
source.re_authenticate()! {
{- ... -}
}
},
_ as e => log("Unhandled fetch error: {}", e'tag_name),
catch e => return Data.default(),
}
result
}
自动枚举融合:
当一个函数调用多个可能返回不同错误集的函数时,编译器可以自动推导并融合所有可能的错误类型,形成当前函数签名所需的 Errors
集合。详见自动枚举融合。
数据结构 (Data Structures)
数据结构是组织和存储数据的方式,是构建任何复杂程序的基础。Flurry 提供了多种内置的数据结构类型,用于表示不同形式的数据集合,并允许开发者定义自己的复杂数据类型。
Flurry 的一个核心设计理念是,类型定义本身就是一个完备的命名空间。这意味着像结构体 (struct
)、枚举 (enum
)、联合体 (union
) 甚至模块 (mod
) 这些用于定义类型的关键字,它们所创建的不仅仅是数据的蓝图,更是一个可以包含常量、静态变量、嵌套类型定义以及关联函数(方法)的作用域。
本章将深入探讨 Flurry 中主要的内置和用户自定义数据结构:
- 结构体 (Structs): 将不同类型的数据组合成一个逻辑单元。
- 枚举 (Enums): 定义一个可以持有多种不同变体之一的类型。
- 联合体 (Unions): 允许多个字段共享同一块内存空间(需要谨慎使用)。
- 数组与切片 (Arrays & Slices): 处理固定大小和动态大小的同质数据序列。
- 元组 (Tuples): 轻量级的、匿名的、固定大小的异质数据序列。
- 模块作为类型 (Modules as Types): 理解模块本身也是一种包含成员的类型。
- Newtypes: 创建基于现有类型的新类型,以增强类型安全。
理解这些数据结构及其作为命名空间的行为,是掌握 Flurry 类型系统和组织复杂代码的关键。
结构体 (Structs)
结构体(Struct)是 Flurry 中最基本的用户自定义数据类型之一。它允许你将多个不同类型的值组合在一起,形成一个有意义的、命名的复合类型。
定义结构体
使用 struct
关键字来定义结构体,并在花括号 {}
内声明其字段(成员变量)。每个字段都有一个名称和一个类型注解。
struct Point {
x: f64,
y: f64,
}
struct Color {
red: u8,
green: u8,
blue: u8,
}
struct UserProfile {
username: String,
email: String,
is_active: bool,
login_count: u64,
}
实例化结构体
定义了结构体后,你可以创建它的实例。Flurry 使用类似 TypeName { .field1 value1, ... }
的语法来初始化结构体实例,这正是我们之前讨论过的 "双变参调用" 语法的一种应用,这里没有传递 "children",只传递了 "attributes"(字段)。
let origin = Point { .x 0.0, .y 0.0 }
let white = Color { .red 255, .green 255, .blue 255 }
let user = UserProfile {
.username "alice".to_string(),
.email "alice@example.com".to_string(),
.is_active true,
.login_count 5,
}
字段初始化的顺序通常不重要,但必须提供所有非可选字段的值(除非它们有默认值,这需要语言支持)。
访问字段
使用点 (.
) 操作符来访问结构体实例的字段。
println("Origin point: ({}, {})", origin.x, origin.y); -- 输出: Origin point: (0.0, 0.0)
let user_email = user.email;
如果字段是可变的 (mut
),且实例绑定也是可变的 (let ...
),则可以修改字段的值:
let user = user;
user.login_count += 1;
结构体作为命名空间
如前所述,struct
定义本身就是一个命名空间。你可以在 struct
定义的花括号内直接定义关联函数(通常称为方法)。方法通常接收 *self
(指针), 或 self
(获取所有权) 作为第一个参数,用于访问或修改实例数据。
struct Rectangle {
width: u32,
height: u32,
pub fn area(*self) -> u32 {
self.width * self.height -- 通过 self 访问实例字段
}
pub fn square(size: u32) -> Self { -- Self 是类型的别名
Rectangle { .width size, .height size }
}
}
test {
let rect = Rectangle { .width 10, .height 5 }
println("Area: {}", rect.area()); -- 调用实例方法: 输出 Area: 50
let sq = Rectangle.square(8);
println("Square area: {}", sq.area()); -- 输出 Square area: 64
}
- 方法定义在
struct { ... }
内部。 - 使用
.
操作符调用实例方法 (rect.area()
)。 - 使用
.
调用选取符号 (Rectangle.square(8)
)。
组合与委托 (using
)
结构体可以通过 using
关键字将内部字段的成员暴露出来,简化接口。
struct Person {
name: String,
pub fn greet(*self) { println("Hello, I'm {}", self.name); }
}
struct Employee {
person: Person,
employee_id: u32,
using person.*; -- 将 Person 的 greet 方法提升到 Employee
}
test {
let emp = Employee {
.person Person { .name "Bob".to_string() },
.employee_id 123,
}
emp.greet(); -- 直接调用 greet,无需 emp.person.greet()
-- 输出: Hello, I'm Bob
}
结构体是 Flurry 中构建复杂数据模型的核心工具,结合其作为命名空间的能力和属性系统,提供了强大的定制化和组织能力。
枚举 (Enums)
枚举(Enum)是 Flurry 中用于定义一个“类型可以是多种可能变体之一”的数据结构。它允许你将一组相关的、但结构可能不同的数据类型聚合在一个统一的类型名下。
定义枚举
使用 enum
关键字定义枚举,并在花括号 {}
内列出其所有可能的变体 (variants)。每个变体可以:
- 只是一个简单的名字(像 C 语言的枚举)。
- 包含关联数据(像 Rust 或 Swift 的代数数据类型 ADT)。
-- 简单的状态枚举
enum Status {
ok,
pending,
error,
}
-- 带有数据的枚举 (表示不同形状)
enum Shape {
circle(radius: f64),
rectangle(width: f64, height: f64),
point, -- 也可以有不带数据的变体
}
-- 层级化枚举 (之前讨论过)
enum AstNode {
expr.{ -- 使用 '.' 定义层级
literal(LiteralKind),
binary_op(Op, *AstNode, *AstNode),
},
stmt.{
let_binding(String, *AstNode),
return_stmt(?*AstNode),
},
}
实例化枚举
实例化枚举需要指定要创建的变体,并提供其所需的关联数据(如果有的话)。访问变体通常使用 EnumName.VariantName
的形式。
let current_status = Status.pending;
let my_shape = Shape.rectangle(10.0, 5.5);
let origin_point = Shape.point;
let node = AstNode.expr.literal(LiteralKind.integer(10));
模式匹配枚举
枚举最常用的场景是与模式匹配(match
, if is
, while is
)结合使用,以根据枚举实例当前持有的变体来执行不同的代码。
fn process_status(status: Status) {
if status is {
.ok => println("Status is OK."),
.pending => println("Status is Pending."),
.error => println("Status is Error."),
-- `match` 需要是穷尽的
}
}
fn calculate_area(shape: Shape) -> f64 {
if shape is {
.circle(r) => 3.14159 * r * r, -- 解构出关联数据 r
.rectangle(w, h) => w * h, -- 解构出 w 和 h
.point => 0.0,
}
}
test {
process_status(current_status); -- 输出: Status is Pending.
let area = calculate_area(my_shape); -- area 会是 55.0
println("Shape area: {}", area);
}
枚举作为命名空间与关联函数
与结构体类似,enum
定义本身也是一个命名空间,可以在其 {}
内部定义关联函数(方法)或静态函数。
enum IpAddr {
v4(u8, u8, u8, u8),
v6(String), -- 简化表示
pub fn display(*self) {
if self is {
.v4(a, b, c, d) => println("{}.{}.{}.{}", a, b, c, d),
.v6(s) => println("{}", s),
}
}
pub fn loopback_v4() -> Self {
IpAddr.v4(127, 0, 0, 1)
}
}
test {
let home = IpAddr.loopback_v4();
home.display(); -- 输出: 127.0.0.1
let work = IpAddr.v6("::1".to_string());
work.display(); -- 输出: ::1
}
层级化与融合
Flurry 支持层级化枚举 (enum.group.variant
) 和枚举融合 (meta.Enum.from(...)
),提供了强大的组织和组合枚举的能力。(详见 层级化与融合)
枚举是 Flurry 中表示“和或”类型(Sum Types)的关键工具,结合模式匹配,使得处理具有多种可能形态的数据变得安全和方便。
层级化与融合
Flurry 的枚举 (enum
) 不仅仅是像 C 或 Java 中那样简单的标签列表,也不完全等同于 Rust 中强大的代数数据类型 (ADT)。Flurry 在枚举的设计上引入了两个极具特色的创新:层级化定义 (Hierarchical Definition) 和 枚举融合 (Enum Fusion)。这两个特性相互配合,极大地增强了枚举的组织能力和组合性,尤其是在处理复杂状态空间或像错误类型这样需要组合的场景时。
1. 层级化枚举:给变体一个“家谱”
想象一下,你在为编译器定义抽象语法树 (AST) 节点类型。你可能会有各种二元操作符(加、减、乘、除、与、或、比较等)、一元操作符(取反、负号)、字面量(整数、浮点数、字符串)等等。如果把它们都平铺在一个巨大的枚举里,列表会很长,而且逻辑分组也不明显。
Flurry 的层级化枚举允许你像组织文件目录一样来组织枚举的变体:
enum AstNodeKind {
-- 使用 '.' 来创建层级
unary.{ -- 'unary' 组
bool_not, -- 路径是 unary.bool_not
neg, -- 路径是 unary.neg
},
binary.{ -- 'binary' 组
add, sub, mul, div, -- 直接成员:binary.add 等
bool.{ -- 'binary' 下的 'bool' 子组
and, -- 路径是 binary.bool.and
or,
eq, ne, lt, le, gt, ge, -- binary.bool.eq 等
},
-- 可以继续嵌套其他二元操作符分组...
},
literal.{ -- 'literal' 组
int, float, string, char, -- literal.int 等
},
variable_ref, -- 顶层变体
function_call, -- 顶层变体
-- ... 其他节点类型 ...
}
-- 如何使用?
test {
let node_type = AstNodeKind.binary.bool.eq; -- 使用完整的层级路径访问变体
if node_type is {
-- 可以在模式匹配中使用层级
.binary.bool.eq => println("Equality comparison"),
-- 使用通配符匹配整个分组
.binary.bool.* => println("Some boolean binary operation"),
.unary.* => println("Some unary operation"),
.literal.int => println("Integer literal"),
_ => println("Other node type"),
}
}
- 语法: 使用点
.
和花括号{}
来创建层级。点号用于分隔层级名称,花括号用于包含子层级或同一层级的多个变体。 - 优点:
- 组织性: 极大地提高了大型枚举的可读性和可维护性。
- 命名空间: 天然地为变体提供了命名空间,减少命名冲突(例如,可以有
binary.add
和vector.add
而不冲突)。 - 逻辑分组: 枚举的结构可以直接反映概念上的分类。
- 模式匹配增强: 可以在
match
或if is
中使用层级路径和通配符 (*
) 进行更结构化的匹配。
- 本质: 尽管看起来有层级,但在底层实现中,每个最终的变体(如
binary.bool.and
)仍然是一个唯一的标签或标识符。层级化主要体现在语法层面的组织和访问方式上。
2. 枚举融合:将不同的世界合并
现在,假设不同的库或模块定义了各自的枚举,比如网络库定义了 NetErr
,文件系统库定义了 FsErr
。在某个函数中,你可能同时遇到这两种错误。你需要一种方法来统一处理它们,但又不想手动创建一个包含所有可能错误的新枚举(这很繁琐且难以维护)。
这就是枚举融合 (Enum Fusion) 发挥作用的地方。Flurry 允许你在编译时将多个独立的枚举**“融合”**成一个新的、统一的枚举类型。
-- --- 在库 A 中定义 ---
mod lib_a {
pub enum ErrorA {
Timeout,
ConnectionFailed,
}
derive Eq for ErrorA;
}
-- --- 在库 B 中定义 ---
mod lib_b {
pub enum ErrorB {
NotFound,
PermissionDenied,
}
derive Eq for ErrorB;
}
-- --- 在使用代码中融合 ---
use lib_a.ErrorA;
use lib_b.ErrorB;
-- 使用编译时元函数 (假设语法) 创建一个新的融合枚举类型 C
-- C 现在包含了来自 ErrorA 和 ErrorB 的所有变体,并保留了它们的“来源”信息
-- 自动实现相关Into与From
newtype C = meta.Enum.from([ErrorA, ErrorB]);
derive Eq for C;
-- 演示融合后的类型
fn might_fail_combined() -> C {
if condition1 {
ErrorA.Timeout.into()
} else if condition2 {
ErrorB.NotFound.into();
} else {
-- ... 其他情况 ...
}
}
test {
let result: C = might_fail_combined();
-- 可以使用原始枚举的层级路径 (加上来源) 来匹配融合后的枚举
if result is {
.ErrorA.Timeout => println("Got timeout from Lib A"),
.ErrorA.ConnectionFailed => println("Got connection failed from Lib A"),
.ErrorB.NotFound => println("Got not found from Lib B"),
.ErrorB.PermissionDenied => println("Got permission denied from Lib B"),
-- 也可以用通配符匹配来自特定源的所有错误
.ErrorA.* => println("Some other error from Lib A"),
.ErrorB.* => println("Some other error from Lib B"),
}
-- 验证身份:融合后的值与原始值不同
let original_a = ErrorA.Timeout;
let fused_a: C = original_a.into();
asserts original_a != fused_a;
}
- 机制: 通过一个编译时计算接收一个枚举类型列表,并在编译时生成一个新的枚举类型。
- 保留来源: 生成的融合枚举不仅包含所有源枚举的变体,还保留了每个变体来自哪个源枚举的信息。这体现在访问和匹配融合枚举时,通常需要带上源枚举的名称或路径(如
.ErrorA.Timeout
)。 - 类型转换: 提供了从源枚举类型到融合枚举类型的转换方法(如
.into()
)。 - 组合性: 极大地提高了代码的组合性。不同的模块可以独立定义自己的枚举,然后在需要统一处理的地方将它们融合起来,而无需修改原始定义。
- 错误处理应用: 枚举融合在错误处理 (
![Err1, Err2] T
) 中的应用最为典型。编译器自动为你执行这种融合,创建匿名的错误联合类型来无缝地处理来自不同源的错误,并能通过层级路径或通配符精确匹配。
层级化与融合的协同:
这两个特性可以完美结合。如果源枚举本身就是层级化的,那么融合后的枚举也会保留这种层级结构,只是在最外层增加了一个表示来源的层级。例如,如果 ErrorA
有 .network.timeout
,融合后可能匹配 .ErrorA.network.timeout
。
总结:
Flurry 通过层级化枚举提供了强大的内部组织能力,使得大型枚举易于管理和理解。通过枚举融合,它又提供了无与伦比的外部组合能力,允许在不修改原始定义的情况下合并不同的枚举类型。这两个特性共同作用,特别是在构建复杂的错误类型、状态机、AST 节点等方面,提供了极大的便利性和表达力,是 Flurry 类型系统设计中的一大亮点。
枚举属性
Tagged Polymorphism
联合体 (Unions)
联合体(Union)是一种特殊的数据结构,它允许其多个字段共享同一块内存区域。这意味着在任何时候,联合体实例只能有效地持有其一个字段的值。联合体主要用于与 C 语言代码交互 (FFI),或者在需要极度节省内存且能安全管理活跃字段的底层编程场景。
警告: 使用联合体需要开发者自行承担跟踪哪个字段当前是活跃(有效)的责任。错误地访问非活跃字段可能导致未定义行为或内存损坏。因此,除非有充分理由并能确保安全,否则应优先使用枚举(Enum)来表示“多种可能之一”的数据。
定义联合体
使用 union
关键字定义联合体,并在花括号 {}
内声明其所有可能的字段。
-- 一个可能持有整数或浮点数的联合体
union IntOrFloat {
i: i32,
f: f32,
}
-- 用于模拟 C 语言联合体进行 FFI
^repr(.c) -- (假设) 指定 C 兼容布局
union CEventArgs {
key_event: KeyEvent, -- 假设 KeyEvent 是一个 struct
mouse_event: MouseEvent, -- 假设 MouseEvent 是一个 struct
}
实例化和访问联合体
联合体的实例化通常需要明确指定要初始化的那个字段。访问字段也使用点 (.
) 操作符,但必须确保访问的是当前活跃的字段。
test {
-- 初始化为整数
let value = IntOrFloat { .i 10 }
-- 安全地访问整数(因为我们知道它是活跃的)
println("Integer value: {}", value.i);
-- **危险操作**:写入浮点字段会覆盖整数数据
value.f = 3.14;
-- **危险操作**:此时再读取 i 字段会得到未定义或损坏的数据
-- println("Integer value after float write: {}", value.i); -- <-- 未定义行为!
-- 安全地访问浮点数
println("Float value: {}", value.f);
}
安全使用联合体:带标签的联合体 (Tagged Unions)
为了安全地使用联合体,常见的模式是将其与一个枚举(作为标签)组合在一个结构体中,用标签来明确指示当前哪个联合体字段是活跃的。Flurry 的枚举 (Enum) 本身就是一种更安全、更推荐的带标签联合体的实现方式。
-- 使用枚举代替裸联合体是更安全的方式
enum SafeIntOrFloat {
integer(i32),
float(f32),
}
test {
let value = SafeIntOrFloat.integer(10);
match value {
.integer(i) => println("Integer: {}", i),
.float(f) => println("Float: {}", f),
}
}
联合体作为命名空间与关联函数
与 struct
和 enum
一样,union
定义也是一个命名空间,可以在其内部定义关联函数。但这通常不太常见,因为操作联合体实例需要外部信息(哪个字段是活跃的)。
总结
联合体提供了底层内存布局的控制能力,允许多个字段共享内存,主要用于 FFI 和特殊的内存优化场景。然而,由于其固有的不安全性(需要手动跟踪活跃字段),强烈建议优先使用枚举 (Enum) 来表示互斥的数据变体。如果必须使用联合体,务必采取额外的机制(如外部标签)来确保访问安全。
数组与切片 (Arrays & Slices)
数组(Array)和切片(Slice)是 Flurry 中用于处理连续内存序列的核心数据结构。它们都包含相同类型的元素。
数组 (Array)
数组是一组固定大小的、存储在栈上或作为其他数据结构一部分的同质元素序列。数组的大小是其类型的一部分。
定义与实例化:
数组类型通常表示为 Array<T, N>
,其中 T
是元素类型,N
是数组长度(一个编译时usize)。
-- 定义一个包含 5 个 i32 的数组
let numbers: Array<i32, 5> = undefined; -- `undefined`不进行初始化
-- 使用字面量初始化数组
let first_primes: Array<i32, 5> = [2, 3, 5, 7];
-- 创建一个包含 100 个 0 的数组
let zeros: Array<i8, 100> = Array.fill(0);
- 数组的大小 (
N
) 必须在编译时确定。 - 数组的所有元素在内存中是连续存储的。
访问元素:
使用方括号 []
和索引(从 0 开始)来访问数组元素。
let third_prime = first_primes[2]; -- 访问索引为 2 的元素 (值为 5)
println("Third prime: {}", third_prime);
let array = [10, 20, 30];
array[0] = 15; -- 修改第一个元素
数组长度:
let len = first_primes.len();
println("Length of first_primes: {}", len); -- 输出: 4
切片 (Slice)
切片是对数组(或其他连续内存区域,如 Vec
的内部缓冲区)的一个引用或视图。切片本身不拥有数据,它只是“借用”了一段连续的元素。切片的大小在运行时确定。
切片类型:
切片类型通常表示为 Slice<T>
或类似的泛型类型,其中 T
是元素类型。一个切片实例内部通常包含一个指向序列起始元素的指针和一个长度信息。
创建切片:
切片通常通过对数组或其他支持切片操作的数据结构(如 Vec
)进行“切片”操作来创建。
let array = [1, 2, 3, 4, 5];
-- 创建一个包含所有元素的切片
let full_slice: Slice<i32> = array[..]; -- 引用整个数组
-- 创建一个包含索引 1 到 3 (不含 3) 的元素的切片
let partial_slice: Slice<i32> = array[1..3]; -- 引用 [2, 3]
-- 创建从索引 2 到末尾的切片
let tail_slice: Slice<i32> = array[2..]; -- 引用 [3, 4, 5]
-- 创建从开始到索引 3 (不含 3) 的切片
let head_slice: Slice<i32> = array[..3]; -- 引用 [1, 2, 3]
使用切片:
切片可以像数组一样使用索引访问其元素,但边界检查是在运行时进行的。
println("First element of partial_slice: {}", partial_slice[0]); -- 输出: 2
-- partial_slice[2] -- 这会导致运行时错误 (越界)
let slice_len = partial_slice.len(); -- 获取切片长度
println("Length of partial_slice: {}", slice_len); -- 输出: 2
切片经常用作函数参数,以接受任何长度的同质序列,而无需知道其底层是数组还是 Vec
。
-- 这个函数可以接受任何 i32 的切片
fn sum(numbers: Slice<i32>) -> i32 {
let total = 0;
for num in numbers { -- 可以直接迭代切片
total += num;
}
total
}
test {
let arr = [1, 2, 3];
let vec_data = Vec<i32>.from([4, 5, 6]); -- 假设 Vec 可以创建
println("Sum of arr slice: {}", sum(arr[..])); -- 传递数组切片
println("Sum of vec slice: {}", sum(vec_data.slice())); -- 假设 Vec 提供切片方法
}
总结
数组是固定大小的、拥有数据的序列,而切片是动态大小的、借用数据的视图。数组的大小是类型的一部分(编译时确定),切片的大小是运行时确定的。切片提供了处理不同来源连续序列的统一、灵活的方式。
元组 (Tuples)
元组(Tuple)是一种简单、匿名的复合数据类型,用于将固定数量的、可能不同类型的值组合在一起。元组的大小(元素数量)和每个元素的类型在编译时确定。
定义与实例化:
元组使用圆括号 ()
将其元素括起来,元素之间用逗号 ,
分隔。元组的类型由其包含的元素类型和顺序决定。
-- 创建一个包含 i32 和 f64 的元组
let pair: (i32, f64) = (10, 3.14);
-- 类型可以省略,编译器会自动推断
let another_pair = (-5, true); -- 类型是 (i32, bool)
-- 包含不同类型的元组
let mix = (1, "hello", false, 3.0f32); -- 类型是 (i32, str, bool, f32)
-- 单元元组 (Unit Tuple) / 空元组
let empty: () = (); -- 类型是 `void`,只有一个值 ()
访问元素 (解构与索引):
访问元组成员主要有两种方式:
-
解构绑定 (Destructuring Bind): 使用
let
和模式匹配将元组的元素直接绑定到变量。这是最常用的方式。let (integer_part, float_part) = pair; -- 解构 pair println("Integer: {}, Float: {}", integer_part, float_part); -- 输出: Integer: 10, Float: 3.14 let (status_code, _, message, _) = mix; -- 使用 _ 忽略不需要的元素 println("Status: {}", status_code);
-
索引访问 (Index Access): 访问特定位置的元素。
let first_element = pair[0]; -- 访问第一个元素 (10) let second_element = pair[1]; -- 访问第二个元素 (3.14) println("Access via index: {}, {}", first_element, second_element);
元组作为函数返回值:
元组非常适合用作函数的返回值,以一次性返回多个不同类型的值。
fn divide_with_remainder(dividend: i32, divisor: i32) -> (i32, i32) {
let quotient = dividend / divisor;
let remainder = dividend % divisor;
(quotient, remainder) -- 返回一个包含商和余数的元组
}
test {
let (q, r) = divide_with_remainder(10, 3);
println("10 / 3 = {} remainder {}", q, r); -- 输出: 10 / 3 = 3 remainder 1
}
单元类型/空类型 void
:
空类型 void
也称为单元类型 (Unit Type)。它只有一个值 ()
。当函数不返回任何有意义的信息时,通常隐式或显式地返回 ()
类型。
总结
元组提供了一种轻量级的方式来组合固定数量、可能不同类型的值。它们在需要临时组合数据或从函数返回多个值时非常有用。通过解构绑定和索引访问可以方便地使用元组成员。
模块 (Modules)
在 Flurry 中,每个类型都有独立命名空间,所以都可以被视为模块。模块的成员可以是其他模块、函数、常量、类型、全局变量等。
而mod
类型则是专门用于表示模块的类型。每个文件都是一个模块,每个目录也是一个模块,其内容由目录下的mod.fl
文件定义。
模块定义:
模块使用 mod
关键字定义,通常与文件系统结构相关联(例如,一个目录对应一个模块,mod.fl
作为入口文件)。
-- src/utils/mod.fl
mod string; -- 使用 'mod' 语句引入同目录下的 string.fl
mod math; -- 引入 math.fl
pub fn helper_function() { {- ... -} }
const VERSION = "1.0";
-- src/utils/string.fl
pub fn trim(s: String) -> String { {- ... -} }
-- src/utils/math.fl
pub fn add(a: i32, b: i32) -> i32 { a + b }
-- --- main.fl ---
mod utils; -- 引入 src/utils/ 目录下的模块
test {
-- 像访问类型成员一样访问模块成员
let version = utils.VERSION;
utils.helper_function();
let sum = utils.math.add(1, 2);
let trimmed = utils.string.trim(" hello ".to_string());
}
总结
Flurry 将模块提升到了与 struct
, enum
等类似的“类型”地位,使其成为编译时可知、可包含各种成员的命名空间实体。这种设计统一了成员访问语法,并为强大的编译时元编程能力(如模块反射)奠定了基础。
Newtypes
Newtype 是一种创建新类型的方式,该新类型在底层表示上基于一个已存在的类型,但在类型系统层面被视为一个完全不同的类型。它主要用于增强类型安全,通过创建独特的类型来区分原本表示相同但逻辑意义不同的值。
定义 Newtype
使用 newtype
关键字来定义。
-- 定义一个 Newtype UserId,底层是 Uuid
newtype UserId = Uuid;
-- 自动derive From<Uuid> for Uuid,derive Into<Uuid> for UserId
-- 定义一个 Newtype EmailAddress,底层是 String
newtype EmailAddress = String;
-- 也可以基于复杂类型
struct ProductId { value: u64 }
newtype ProductRef = *ProductId;
类型安全优势
Newtype 的核心价值在于类型安全。即使底层表示相同,不同 Newtype 之间也不能直接混用。
fn process_user(id: UserId) { {- ... -} }
fn process_product(id: ProductId) { {- ... -} } -- 假设 ProductId 是一个 struct
test {
let user_uuid = Uuid.new_v4();
let product_id_val = 12345u64;
let user_id = user_uuid.into();
let product_id = ProductId { .value product_id_val }
process_user(user_id); -- OK
-- process_user(user_uuid); -- 错误! 类型不匹配 (Uuid vs UserId)
-- process_user(product_id); -- 错误! 类型不匹配 (ProductId vs UserId)
-- let temp_id = product_id_val.into();
}
底层表示与开销
Newtype 通常是零成本抽象 (zero-cost abstraction)。在编译后,Newtype 与其底层类型在内存表示和运行时行为上通常是完全相同的。类型检查只发生在编译时。这意味着使用 Newtype 不会带来额外的运行时性能开销。
为 Newtype 实现 Trait 或方法
可以像为普通 struct
或 enum
一样,为 Newtype 定义implementation和extension。
定义newtype时,不仅编译器会自动为其实现 From
和 Into
trait,还可以通过type casting 来实现类型转换。
newtype Age = u32;
impl Age {
-- 为 Newtype 添加方法
pub fn is_adult(*self) -> bool {
self.as(u32) >= 18
}
}
extend fmt.Display for Age {
fn fmt(*self, f: *fmt.Formatter) -> fmt.Result {
-- 需要访问底层值
write!(f, "{} years old", self.as(u32))
}
}
test {
let age = 30.as(Age);
if age.is_adult() {
println("Is adult. {}", age); -- 输出: Is adult. 30 years old
}
}
Typealias vs Newtype
Newtype 与类型别名(typealias
)的区别在于,类型别名只是给现有类型一个新的名字,而 Newtype 创建了一个新的、独立的类型。类型别名不会提供额外的类型安全,而 Newtype 则会。
总结
Newtype 提供了一种轻量级(通常零成本)的方式来创建新的、类型安全的别名。它们是增强代码清晰度和防止逻辑错误的有力工具,通过在编译时区分不同逻辑含义但底层表示可能相同的值。当你想给一个现有类型赋予更强的语义约束时,Newtype 是一个绝佳的选择。
模式匹配 (Pattern Matching)
模式匹配是 Flurry 语言中一种强大且富有表现力的机制,用于检查一个值是否符合某种结构,并在此过程中可能将值的组成部分解构 (destructure) 并绑定到新的变量上。它不仅可以用于解构复杂的数据结构,还与控制流语句深度集成,提供了简洁、安全的条件判断和循环方式。
Flurry 的模式语法极其丰富,涵盖了从基本字面量到复杂数据结构、范围、组合模式等多种形式。
本章将涵盖:
- 模式语法: 详细介绍 Flurry 支持的各种模式类型及其语法。
- 匹配控制流: 展示模式匹配如何在
match
表达式、if is
语句和while is
循环中驱动程序逻辑。
理解模式匹配是掌握 Flurry 表达力和编写惯用 (idiomatic) Flurry 代码的关键。
模式语法 (Pattern Syntax)
模式 (Pattern) 是 Flurry 中用于匹配值的语法结构。以下是 Flurry 支持的主要模式类型:
1. 基本模式 (Basic Patterns)
这些是最简单的模式,用于匹配字面量或进行简单的绑定/忽略。
- 字面量模式 (Literal Patterns): 直接匹配整数、浮点数、字符、字符串、布尔值或
null
。if value is { 0 => println("Zero"), 'a' => println("The character 'a'"), "hello" => println("Got hello"), true => println("It's true"), null => println("It's null"), _ => println("Something else"), -- 通配符模式 }
- 标识符模式 (Identifier Pattern) / 变量绑定: 匹配任何值,并将其绑定到一个新的变量上。该变量在匹配分支的作用域内可用。
if some_option is { data? => println("Got data: {}", data), -- data 绑定了 some 内部的值 null => println("No data"), }
- 通配符模式 (
_
): 匹配任何值,但不将其绑定到变量。用于忽略不关心的值或部分结构。let (x, _, z) = (1, 2, 3); -- 忽略元组的第二个元素
- 符号模式 (
.symbol
): 匹配特定的符号字面量。常用于匹配枚举变体或Symbol
类型。if status is { .ok => println("Status is Ok"), .error => println("An error occurred"), -- ... }
2. 容器模式 (Container Patterns)
用于解构和匹配复合数据结构。
- 列表/数组/切片模式 (
[pattern1, pattern2, ...]
): 匹配列表、数组或切片。- 可以匹配固定长度:
[first, second]
- 可以使用
...rest_binding
捕获剩余元素(Rest Pattern):[head, ...tail]
- 可以匹配空列表:
[]
if items is { [] => println("Empty list"), [one] => println("One item: {}", one), [first, second, ...rest] => println("First: {}, Second: {}, Rest: {any}", first, second, rest), -- ... }
- 可以匹配固定长度:
- 元组模式 (
(pattern1, pattern2, ...)
): 按位置解构元组。let (id, name, _) = get_user_info(); -- 解构元组,忽略第三个元素
- 记录/结构体模式 (
{ field1: pattern1, field2, ... }
): 按字段名解构结构体或object
。field: pattern
: 将字段field
的值与pattern
匹配。field
: 字段名简写,等同于field: field
,即将字段值绑定到同名变量。...
: (可能支持)用于表示匹配剩余字段,但通常结构体匹配是精确的或显式忽略。
struct Point { x: i32, y: i32 } let p = Point { .x 10, .y 20 } if p is { -- 使用字段名简写和显式模式 { x: 0, y } => println("On Y axis at {}", y), { x, y: 0 } => println("On X axis at {}", x), { x, y } => println("Point at ({}, {})", x, y), }
3. 调用模式 (Call Patterns)
用于匹配枚举变体或构造器调用的结果,常用于解构带有数据的枚举。
- 圆括号调用模式 (
pattern (pattern1, ...)
或pattern (pattern1, ...)
?): 匹配特定的枚举变体(带有关联数据)、代数效应调用。enum Message { quit, write(String), move { x: i32, y: i32 } } if msg is { -- 匹配不带数据的变体 .quit => println("Quit message"), -- 匹配 Write 变体,并将内部 String 绑定到 text .write(text) => println("Write: {}", text), -- 匹配 Move 变体,并使用记录模式解构其关联数据 .move { x, y: 0 } => println("Move on X axis to {}", x), .move { x, y } => println("Move to ({}, {})", x, y), }
- 尖括号调用模式 (
pattern<...>(...)
或pattern<...>(...)
): 用于匹配带有泛型参数的代数效应调用、类型表达式。 - 花括号调用模式 (
pattern{...}
或pattern{...}
): 用于匹配使用记录语法的变体或构造器(如上例中的move
)。
4. 范围模式 (Range Patterns)
用于匹配数值或字符是否落在某个范围内。
- 开区间上限 (
..end
): 匹配小于end
的值。(之前讨论过,可能需要确认语法) - 闭区间 (
start..=end
): 匹配大于等于start
且小于等于end
的值。 - 开区间 (
start..end
): 匹配大于等于start
且小于end
的值。 - 闭区间下限 (
start..
): 匹配大于等于start
的值。if value is { 0..=9 => println("Single digit"), 10..=99 => println("Two digits"), 100.. => println("Three or more digits"), ..0 => println("Negative or zero? Check exact rules"), -- 语法和含义需确认 } if character is { 'a'..='z' => println("Lowercase letter"), 'A'..='Z' => println("Uppercase letter"), _ => println("Not a letter"), }
5. 组合模式 (Combination Patterns)
允许将多个模式组合起来。
- 或模式 (
pattern1 or pattern2
): 如果值匹配pattern1
或pattern2
,则匹配成功。用于合并相似的分支。
注意: 在if character is { 'a' or 'e' or 'i' or 'o' or 'u' => println("Lowercase vowel"), 'A' or 'E' or 'I' or 'O' or 'U' => println("Uppercase vowel"), _ => println("Consonant or other"), }
or
模式中,所有分支绑定的变量必须名称和类型都相同,或者只使用_
忽略。 - 与模式 (
pattern1 and expr is pattern2
): 如果值匹配pattern1
,并且expr
的值匹配pattern2
,则匹配成功。用于在模式中嵌套其他表达式。-- 选课逻辑展示 fn select_course(student: Student, course: Course) { if student is { class_id: class_id? } and get_class(class_id) is class? and get_major(class.major_id) is major? if course.satified(major) { println("选课成功") } }
6. 绑定模式 (pattern as identifier
)
在匹配 pattern
的同时,将整个匹配到的值(未解构的)绑定到一个新的变量 identifier
上。
if message is {
-- 匹配 move 变体,解构 x,并将整个 move 变体绑定到 m
.move { x: 10, y } as msg => {
println("Move to x=10 at y={}", y);
process_move_message(msg); -- 使用绑定的整个消息 msg
}
-- ... 其他分支
}
7. 条件守卫模式 (pattern if condition_expr
)
只有当值匹配 pattern
并且 condition_expr
(一个运行时布尔表达式)求值为 true
时,模式才算匹配成功。condition_expr
可以使用 pattern
中绑定的变量。
if maybe_point is {
{ x, y }? if x == y => { -- 匹配结构体 Point
println("Point on diagonal: ({}, {})", x, y);
},
{ x, y }? => { -- 其他结构体情况
println("Point not on diagonal: ({}, {})", x, y);
}
_ => println("Not a point"),
}
8. 其他模式
- 可选模式 (
pattern?
): 用于匹配可空类型的非空部分,并解构其内部值。(如if opt is data? { ... }
) - 正确模式: 用于匹配结果类型的成功部分,并解构其内部值。(如
if result is data! { ... }
) - 错误模式: 用于匹配结果类型的错误部分,并解构其内部值。(如
if result is error err { ... }
) - 表达式模式 (
<expr>
): 将编译时表达式expr
的求值结果作为模式进行匹配。 - 位向量模式 (
0x(...)
,0o(...)
,0b(...)
): 匹配和解构位向量字面量。 - 字符串前缀模式 (
"prefix" ++ rest_binding
): 匹配以特定前缀开头的字符串。 - 否定模式 (
not pattern
): 匹配不符合pattern
的值。 - 类型绑定模式 (
'id
): 用于匹配类型表达式,为了方便起见,类型模式中的id通常会被解析为表达式,因此变量绑定需要新的语法,如Array<'T, 128>
。
Flurry 提供了极为丰富和灵活的模式语法,为数据检查、解构和控制流提供了强大的支持。
匹配控制流 (Matching Control Flow)
模式匹配不仅限于解构数据,它还深度集成到 Flurry 的控制流语句中,提供了比传统条件判断更强大、更安全的流程控制方式。主要体现在 match
表达式、if is
语句和 while is
循环中。
match
表达式
match
表达式是 Flurry 中进行模式匹配的核心结构。它接收一个表达式的值,然后按顺序尝试将其与一系列模式进行匹配。一旦找到第一个成功匹配的模式,就会执行该模式对应的代码块,并且 match
表达式本身会返回该代码块的求值结果。
基本语法:
<value_expr> match {
<pattern_1> => <result_expr_1>,
<pattern_2> => <result_expr_2>,
-- ... more branches
<pattern_n> => <result_expr_n>,
-- 可以提供一个通配符或变量绑定来确保穷尽性
_ => <default_result_expr>,
}
<value_expr>
: 需要被匹配的值。<pattern_i> => <result_expr_i>
: 一个匹配分支,由模式和对应的结果表达式(或代码块)组成。match
按顺序尝试匹配<pattern_i>
。第一个成功匹配的分支会被选中。- 模式中绑定的变量在对应的
<result_expr_i>
中可用。 - 穷尽性 (Exhaustiveness): Flurry 编译器通常会检查
match
表达式的分支是否覆盖了<value_expr>
类型的所有可能情况。如果不是穷尽的,并且没有通配符_
或变量绑定分支来处理剩余情况,编译器会报错,以防止运行时遗漏某些情况。 - 返回值:
match
作为一个表达式,其本身会有一个值,即被选中分支的<result_expr_i>
的值。所有分支的结果表达式必须具有兼容的类型。
示例:
enum Message { ping, pong, text(String) }
fn process_message(msg: Message) -> String {
let response = msg match {
.ping => "Pong response".to_string(),
.pong => "Ping response".to_string(),
.text(content) if content.len() > 10 => { -- 使用模式守卫
println("Received long text: {}", content);
"Text OK (long)".to_string()
},
.Textextt(content) => {
println("Received short text: {}", content);
"Text OK (short)".to_string()
} -- 这个 match 是穷尽的,覆盖了所有 Message 变体
}
response
}
if is
语句
if is
语句不仅可以可以像match一样匹配多个分支,还提供了一种简洁的方式来检查一个值是否匹配单个模式,并在匹配成功时执行一个代码块。
基本语法:
if <value_expr> is <pattern> {
-- Code to execute if value_expr matches pattern
-- pattern 中绑定的变量在此可用
} else if <value_expr> is <another_pattern> { -- 可选 else if is
-- ...
} else { -- 可选 else
-- Code to execute if no preceding pattern matched
}
- 它尝试将
<value_expr>
与<pattern>
匹配。 - 如果匹配成功,执行第一个
{...}
块,并且模式中绑定的变量在该块内可用。 - 可以链式地使用
else if is
来检查其他模式。 - 可选的
else
块处理所有前面模式都不匹配的情况。
示例 (处理可选类型):
fn print_optional(opt: ?i32) {
if opt is some? { -- 使用可选模式 '?'
println("Value is: {}", some); -- 'some' 自动绑定内部值
} else {
println("Value is None/null.");
}
}
print_optional(Some(10)); -- 输出: Value is: 10
print_optional(None); -- 输出: Value is None/null.
对比 match
与 if is
:
match
为后缀风格,适合在复杂链式表达式中使用。- 当只关心是否匹配一两种特定模式时,
if is
通常更简洁。
while is
循环
while is
循环将模式匹配与 while
循环结合,允许在循环的每次迭代开始时对一个表达式的值进行模式匹配。只有当匹配成功时,循环体才会执行,并且模式绑定的变量可在循环体中使用。
基本语法:
while <value_expr> is <pattern> {
-- Loop body, executed if value_expr matches pattern
-- pattern 中绑定的变量在此可用
}
- 在每次循环迭代前,求值
<value_expr>
并尝试与<pattern>
匹配。 - 如果匹配成功,执行循环体。
- 如果匹配失败,循环终止。
示例 (处理迭代器返回可选类型):
let some_iterator = create_iterator();
while some_iterator.next() is item? {
process_item(item);
}
println("Iterator finished.");
while is
对于消耗迭代器、处理消息队列或其他在循环中需要检查并解构值的场景非常方便和安全。
总结
Flurry 将模式匹配与核心控制流结构(match
, if is
, while is
)深度集成,提供了一种强大、安全且表达力强的方式来根据数据的结构和值来引导程序流程。穷尽性检查(主要在 match
中)进一步增强了代码的健壮性。
Trait 与多态 (Traits & Polymorphism)
Trait 是 Flurry 中定义共享行为的主要方式。它们类似于其他语言中的接口 (Interfaces) 或协议 (Protocols),允许你定义一组方法签名,然后不同的类型可以实现这些方法。Trait 是 Flurry 实现抽象、代码复用以及多态性的核心机制。
多态性(Polymorphism)指的是代码可以处理多种不同类型的值的能力。Flurry trait系统主要通过以下方式支持多态:
- 静态分派 (Static Dispatch): 基于泛型和 Trait 约束,在编译时确定调用哪个具体实现。
- 动态分派 (Dynamic Dispatch): 在运行时根据对象的实际类型确定调用哪个实现,Flurry 提供了两种主要的动态多态机制。
本章将深入探讨:
- Trait 的定义与实现 (
trait
,impl
,derive
),以及孤儿原则。 - 独特的
extend
机制,用于作用域受限的行为扩展和字面量拓展。 - Flurry 支持的动态多态方式:传统的 Trait Object (
dyn Trait
) 与基于枚举的 Tagged Polymorphism。 - 使用
using
实现组合与接口委托。
理解 Trait 和多态机制对于编写可扩展、可维护和抽象良好的 Flurry 代码至关重要。
Trait 定义与实现 (impl, derive)
Trait 用于定义一组类型可以共享的方法签名。它描述了某种抽象行为或能力,而不涉及具体的实现。
定义 Trait
使用 trait
关键字定义一个 Trait,并在其花括号 {}
内声明方法签名。方法签名指定了方法名、参数(包括 self
参数)和返回类型,但没有具体实现。
-- 定义一个描述可被绘制行为的 Trait
trait Drawable {
-- 方法签名,需要一个不可变引用指向实例
fn draw(*self, canvas: *Canvas);
}
-- 定义一个可以计算面积的 Trait
trait Area {
fn area(*self) -> f64;
}
-- Trait 也可以包含关联类型或常量 (如果 Flurry 支持)
trait Container {
-- type Item; -- (假设) 关联类型
-- const DEFAULT_CAPACITY: usize; -- (假设) 关联常量
fn add(*mut self, item: Item); -- 使用关联类型
}
实现 Trait (impl
)
要让一个具体类型(如 struct
或 enum
)拥有 Trait 定义的行为,你需要为该类型实现 (implement) 这个 Trait。使用 impl TraitName for TypeName
语法,并在花括号内提供 Trait 中必要方法的具体实现。
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
-- 为 Circle 实现 Area Trait
impl Area for Circle {
fn area(*self) -> f64 {
3.1415926535 * self.radius * self.radius
}
}
-- 为 Rectangle 实现 Area Trait
impl Area for Rectangle {
fn area(*self) -> f64 {
self.width * self.height
}
}
-- (假设 Drawable 和 Canvas 已定义)
impl Drawable for Circle {
fn draw(*self, canvas: *Canvas) {
-- ... 具体绘制圆形的逻辑 ...
println("Drawing a circle with radius {}", self.radius);
}
}
impl Drawable for Rectangle {
fn draw(*self, canvas: *Canvas) {
-- ... 具体绘制矩形的逻辑 ...
println("Drawing a rectangle {}x{}", self.width, self.height);
}
}
test {
let c = Circle { .radius 5.0 }
let r = Rectangle { .width 4.0, .height 6.0 }
println("Circle area: {}", c.area()); -- 调用 Circle 的 area 实现
println("Rectangle area: {}", r.area()); -- 调用 Rectangle 的 area 实现
}
孤儿原则 (Orphan Rule)
为了保证全局实现的一致性,Flurry(类似 Rust)遵循孤儿原则:
对于一个 Trait
T
和一个类型Type
,impl T for Type
这个实现必须定义在定义T
的那个包中,或者定义在定义Type
的那个包中。不允许在完全独立的第三方包中为外部定义的 Trait 和外部定义的 Type 提供实现。
这个规则确保了任何 impl
都有一个明确的“负责人”,防止不同的库对同一对 Trait/Type 提供冲突的实现。
自动派生 (derive
)
对于一些常见的、具有标准实现模式的 Trait(例如,用于调试输出的 Debug
、用于比较的 Eq
/Ord
、用于复制的 Copy
/Clone
等),Flurry 可能提供 derive
属性,让编译器自动为你的类型生成这些 Trait 的实现代码。
struct Point {
x: i32,
y: i32,
}
derive Debug, Clone, Copy for Point;
test {
let p1 = Point { .x 1, .y 2 }
let p2 = p1.clone();
println("Debug point: {debug}", p2);
}
derive
大大减少了编写样板实现代码的工作量。可派生的 Trait 通常由语言或标准库预定义。
Trait 和 impl
构成了 Flurry 静态抽象和代码共享的基础。通过它们,你可以编写泛型代码,约束泛型参数必须实现特定的 Trait,从而在编译时实现类型安全的多态。
扩展 (extension) 与字面量拓展
虽然孤儿原则对于维护大型项目的一致性至关重要,但在某些情况下,我们可能希望临时性地或者在受限的作用域内为一个类型添加某个 Trait 的行为,即使这违反了孤儿原则。Flurry 提供了 extend
关键字来实现这种受控的扩展。
extend
机制还巧妙地被用作 Flurry 字面量拓展的基础。
extend
关键字
extend Trait for Type
语法允许你在任何作用域内为一个(可能是外部定义的)类型 Type
提供一个(可能是外部定义的) Trait
的实现。
mod external_lib {
pub trait Printer {
fn print_info(*self);
}
pub struct Data { pub value: i32 }
}
mod my_code {
use external_lib.{ Printer, Data }
-- 在 my_code 模块内,为外部类型 Data 实现外部 Trait Printer
-- 这违反了孤儿原则,但 extend 允许这样做
extend Printer for Data {
fn print_info(*self) {
println!("My custom print for Data: value = {}", self.value);
}
}
pub fn process_data(d: Data) {
-- 在这个作用域内,调用 print_info 会使用上面的 extend 实现
d.print_info();
}
}
mod another_code {
use external_lib.{ Printer, Data }
-- 在这个模块,Printer for Data 的 extend 没有被导入,
-- 所以无法调用 d.print_info() (除非 external_lib 本身提供了 impl)
pub fn process_data_again(d: Data) {
-- d.print_info(); -- 编译错误 (假设 external_lib 无 impl)
println("Processing data without custom print.");
}
}
extend
的关键特性:
-
绕过孤儿原则: 允许在任何地方定义实现。
-
作用域限制:
extend
定义的实现仅在其被定义的词法作用域内有效。外部代码默认不受此影响。 -
遮盖 (Shadowing): 如果在同一个作用域内,同时存在一个类型的常规
impl
和一个extend
实现,那么**extend
的实现会优先被使用**,它会“遮盖”住impl
的实现。 -
可命名与导入: 可以将
extend
块赋予一个常量标识符,然后其他模块可以通过use
显式导入这个命名扩展,从而将该扩展实现引入到自己的作用域中。mod extensions { use external_lib.{ Printer, Data } -- 将扩展命名为 PrettyPrinterForData pub const PrettyPrinterForData = extend Printer for Data { fn print_info(*self) { println!("*** Pretty Data: {} ***", self.value); } } } mod user_code { use external_lib.Data; use extensions.PrettyPrinterForData; -- 显式导入扩展 pub fn main() { let d = Data { .value 42 } -- 因为导入了扩展,这里会使用 PrettyPrinterForData 的实现 d.print_info(); -- 输出: *** Pretty Data: 42 *** } }
这种显式导入机制提供了精确的作用域控制。
extend
用于字面量拓展
Flurry 利用 extend
机制实现了一种强大且统一的字面量拓展功能。当你编写类似 10px
或 "hello"c
的代码时,编译器实际上是在查找一个为该字面量对应的内置类型(如 Integer
, u32
, str
)定义的、包含名为 px
或 c
的方法的 extend
块。
原理:
literal + identifier
形式会被编译器尝试解析为 literal.identifier()
的方法调用。这个方法必须由一个在当前作用域内有效的 extend
块提供。
-- 为内置类型 u32 扩展一个 px 方法
extend u32 {
-- 'px' 方法接收 u32 值,返回 Pixel 类型
comptime fn px(self) -> Pixel {
Pixel { .value self } -- 假设 Pixel struct 已定义
}
}
-- 为内置 str 类型扩展一个 cstr 方法
extend str {
-- 'cstr' 方法接收编译时 str,返回某种 C 兼容字符串表示
comptime fn cstr(self) -> CString { -- 假设 CString 类型存在
-- ... 编译时或运行时逻辑来创建 CString ...
CString.from_str_with_null(self)
}
}
test {
let length = 100px; -- 等价于 (100).px()
asserts length.value == 100;
let c_string = "api_key"cstr; -- 等价于 ("api_key").cstr()
-- c_string 现在是一个 CString 实例
}
- 这个机制允许开发者或库为基础类型添加与领域相关的单位或转换,使代码更具表现力。
- 语言可以内置一些常用的拓展,如
"..."cstr
用于 FFI,"..."bytes
用于创建字节序列。 - 这些
extend
实现同样遵循作用域规则,可以被局部定义或通过use
导入。
总结
extend
是 Flurry 提供的一种受控方式,用于在标准 impl
和孤儿原则之外为类型添加行为。它的作用域限制和显式导入机制避免了全局冲突,同时为局部行为定制和实现优雅的字面量拓展提供了强大的支持。使用 extend
时需要注意其作用域和对代码可读性的潜在影响。
动态多态 (dyn Trait
vs tagged_polymorphic
)
动态多态允许在运行时根据对象的实际类型来决定调用哪个方法实现。这对于处理异构集合、实现回调或插件系统等场景非常有用。Flurry 提供了两种主要的动态多态机制,它们各有优劣,适用于不同的场景。
1. Trait Object (dyn Trait
)
Trait Object 是实现动态多态的标准、开放的方式,类似于 Rust 的 dyn Trait
或 C++ 的虚函数机制(通过指针或引用)。
概念:
- 一个
dyn TraitName
类型的值是一个胖指针 (fat pointer)。它包含两部分:- 一个指向实际对象数据的指针。
- 一个指向该对象类型为
TraitName
实现的虚函数表 (vtable) 或等效结构(如接口表 itable)的指针。
- vtable 包含了一系列函数指针,指向该类型为 Trait 定义的每个方法的具体实现。
用法:
你可以创建一个 dyn Trait
类型胖指针,它可以在运行时指向任何实现了该 Trait 的类型的实例。
trait Speaker {
fn speak(*self);
}
struct Dog { name: String }
impl Speaker for Dog {
fn speak(*self) { println!("{} says Woof!", self.name); }
}
struct Cat { name: String }
impl Speaker for Cat {
fn speak(*self) { println!("{} says Meow!", self.name); }
}
test {
let dog = Dog { .name "Buddy".to_string() }
let cat = Cat { .name "Whiskers".to_string() }
-- 创建 Trait Object (通过 Box 指针)
let animals: Vec<dyn Speaker> = Vec.new();
animals.push(Box.new(dog).*.dyn(Speaker));
animals.push(Box.new(cat).*.dyn(Speaker));
-- 动态调用方法
for animal in animals.iter() {
-- animal 是 dyn Speaker 类型
-- 调用 speak 时,会通过 vtable 查找并执行 Dog 或 Cat 的实现
animal.speak();
}
-- 输出:
-- Buddy says Woof!
-- Whiskers says Meow!
}
-- 作为函数参数
fn make_speak(speaker: dyn Speaker) {
speaker.speak(); -- 动态分派
}
优点:
- 开放集合 (Open Set): 任何类型,在任何地方(遵守孤儿原则或使用
extend
),只要实现了TraitName
,就可以在运行时被视为dyn TraitName
。库可以定义 Trait,使用者可以自由实现并传递给库,扩展性极好。 - 解耦: 调用者只需要知道
dyn Trait
接口,无需关心具体实现类型。
缺点:
- 运行时开销:
- 间接调用: 方法调用需要通过 vtable 进行间接查找,比静态分派慢。
- 无法内联: 编译器无法内联
dyn Trait
的方法调用。 - 胖指针开销:
dyn Trait
指针(或引用)本身比普通指针占用更多空间(通常是两倍)。
- 类型信息部分丢失: 在
dyn Trait
上下文中,对象的具体类型信息在编译时丢失了(虽然运行时可以通过 RTTI 查询,但通常不鼓励)。
2. Tagged Polymorphism (基于枚举的半自动多态)
Flurry 提供了一种替代的、基于标签枚举的动态多态机制,通过在 enum
定义上使用 .tagged_polymorphic TraitName
属性来启用。
概念:
- 载体: 一个枚举(标记为
.dst true
和.tagged_polymorphic TraitName
)包含所有需要参与此多态的固定类型集合作为其变体。 - 实现: 每个枚举变体对应的具体类型(如
Dog
,Cat
)必须分别实现目标 Trait (Speaker
)。 - 分派: 当通过枚举实例(通常是指针
*EnumName
或Box<EnumName>
)调用 Trait 方法时,分派逻辑基于枚举的内部标签 (tag)。编译器(或运行时)检查标签,确定当前是哪个变体,然后直接调用该变体类型对应的 Trait 实现。这通常通过编译时生成的match
/switch
或跳转表完成。
用法:
trait Greeter {
fn greet(*self) -> String;
}
struct EnglishGreeter {}
impl Greeter for EnglishGreeter { fn greet(*self) -> String { "Hello!".to_string() } }
struct SpanishGreeter {}
impl Greeter for SpanishGreeter { fn greet(*self) -> String { "¡Hola!".to_string() } }
-- 定义 Tagged Polymorphic 枚举
enum AnyGreeter {
.dst true, -- 表明是动态大小
.tagged_polymorphic Greeter, -- 启用基于标签的多态,目标 Trait 是 Greeter
english: EnglishGreeter, -- 包含具体类型作为变体
spanish: SpanishGreeter,
}
test {
-- 创建实例 (需要通过指针或 Box)
let greeter1 = Box.new(AnyGreeter.english(EnglishGreeter {}));
let greeter2 = Box.new(AnyGreeter.spanish(SpanishGreeter {}));
let greeters: Vec<Box<AnyGreeter>> = Vec.new();
greeters.push(greeter1);
greeters.push(greeter2);
for g in greeters.iter() {
-- 调用 greet 方法。分派基于 g 指向的 AnyGreeter 实例的 tag
println!("{}", g.greet());
}
-- 输出:
-- Hello!
-- ¡Hola!
}
-- 作为函数参数 (注意类型是具体的枚举指针)
fn perform_greeting(g: *AnyGreeter) {
println!("{}", g.greet()); -- 基于 tag 分派
}
优点:
- 潜在性能优势:
- 分派后通常是直接调用,避免了 vtable 的间接性。
- 无 vptr 开销: 对象本身不存储额外的 vptr。
- 分支预测可能更友好。
- 类型信息保留: 运行时可以轻易检查枚举 tag,获知具体变体类型。
缺点:
- 封闭集合 (Closed Set): 最大的限制。所有需要参与多态的类型必须预先在枚举中定义。无法在枚举之外添加新的实现类型。
- 修改不便: 添加新类型需要修改枚举定义。
- 适用性: 主要适用于类型集合已知且稳定的场景。
comptime
的作用: Flurry 强大的 comptime
能力可以缓解封闭集合的问题。库可以提供 comptime
元函数,允许使用者在编译时动态生成一个包含他们所需类型列表的 tagged_polymorphic
枚举,从而在终点代码中获得性能优势,而库本身接口可能仍然使用 dyn Trait
。
总结: 何时选择?
- 库接口 / 开放扩展性: 优先选择
dyn Trait
。它提供了必要的灵活性和解耦。 - 性能关键 / 类型集合固定: 在应用程序内部或性能瓶颈处,如果涉及的类型集合是已知的、有限的,可以考虑使用
tagged_polymorphic
枚举以获得潜在的性能提升和更明确的类型信息。 - 编译时定制: 结合
comptime
,可以在编译时生成定制的tagged_polymorphic
枚举,为特定场景提供优化。
Flurry 同时提供这两种机制,让开发者可以根据具体需求在开放性和性能/类型精确性之间做出权衡。
组合与委托 (using
)
面向对象编程中,通常推荐“组合优于继承”。组合是指一个类型通过包含其他类型的实例来复用其功能。然而,简单组合后,外部类型需要手动编写大量的“转发”或“委托”方法才能将内部对象的功能暴露出来。
Flurry 提供了 using
关键字,作为一种便捷的语法糖,用于将结构体内部字段的公开 (public) 成员(方法和可能的字段)自动“提升”或“委托”到外部结构体的接口上。
基本语法:
在 struct
定义内部使用:
using <field_name>.*;
<field_name>
: 必须是当前结构体的一个字段。.*
: 表示将该字段类型的所有公开成员引入到当前结构体的接口中。
示例:
struct Engine {
horsepower: u32,
pub fn start(*self) { println("Engine started!"); {- ... -} }
pub fn stop(*self) { println("Engine stopped."); {- ... -} }
}
struct Transmission {
gear_count: u8,
pub fn shift_up(*self) { {- ... -} println("Shifted up."); }
pub fn shift_down(*self) { {- ... -} println("Shifted down."); }
}
struct Car {
engine: Engine,
transmission: Transmission,
color: String,
-- 将 engine 的所有 pub 方法 (start, stop) 引入 Car 的接口
using engine.*;
-- 将 transmission 的所有 pub 方法 (shift_up, shift_down) 引入 Car 的接口
using transmission.*;
-- Car 也可以有自己的方法
pub fn drive(*self) {
self.start(); -- 直接调用来自 Engine 的 start 方法
println("Driving a {} car.", self.color);
}
}
test {
let my_car = Car {
.engine Engine { .horsepower 200 },
.transmission Transmission { .gear_count 6 },
.color "Red".to_string(),
}
my_car.drive(); -- 调用 Car 自己的方法,内部调用了 engine.start()
my_car.shift_up(); -- 直接调用来自 Transmission 的方法
my_car.stop(); -- 直接调用来自 Engine 的方法
}
-- 可能输出:
-- Engine started!
-- Driving a Red car.
-- Shifted up.
-- Engine stopped.
工作机制:
using field.*
并不实际复制成员。它更像是在编译时为外部结构体自动生成了转发方法(对于方法)或代理访问(对于字段,如果字段也是 pub 的话)。当调用 my_car.start()
时,编译器知道 start
来自 engine
字段,并生成调用 my_car.engine.start()
的代码。
优点:
- 减少样板代码: 极大地简化了将内部组件功能暴露给外部的过程。
- 促进组合: 使基于组合的设计模式更加易于实现和使用。
- 接口清晰 (某种程度上): 调用点的代码更简洁。
注意事项与潜在问题:
- 命名冲突: 如果
using
了多个字段,并且它们导出的成员有同名,或者外部结构体本身定义了同名成员,Flurry编译器将会报错并拒绝编译。解决方法是使用using field.{member1, member2}
语法来选择性导出成员,或者使用不同的名称。 - 可读性: 调用者可能需要查看结构体定义中的
using
语句才能确定某个方法或字段的实际来源。IDE 的支持对于缓解这个问题很重要。 - 选择性导出: 语法
using field.*
导出了所有公开成员。flurry还支持更精细的控制,例如using field.{member1, method2}
, 这可以减少不必要的接口暴露和命名冲突。 - 访问权限:
using
只导出源字段类型的公开 (public) 成员。
总结
using
关键字是 Flurry 提供的一种简化组合模式的有效工具。它通过自动委托内部对象的公开成员,减少了样板代码,使得基于组合的设计更加便捷。在使用时,需要注意潜在的命名冲突和对代码可读性的影响,并依赖语言清晰的规则来管理这些问题。
编译时计算 (comptime
)
Flurry 的一个核心且强大的特性是编译时计算 (Compile-Time Computation),通常以关键字 comptime
体现。该机制允许开发者在编译阶段执行代码,从而实现高级别的元编程、优化和类型安全保证。
Flurry 的两级计算模型
理解 comptime
的基础在于认识 Flurry 的两级计算模型 (Two-Level Computation Model),该模型明确区分了两个不同的执行阶段:
- 编译时 (Comptime): 编译器工作的阶段。在此阶段,Flurry 不仅执行传统的编译任务(如语法分析、类型检查、代码生成),还提供了一个功能强大的编译时运行时 (Comptime Runtime),用于执行标记为
comptime
的代码。 - 运行时 (Runtime): 编译产物(即可执行程序)被最终用户执行的阶段。
这种设计使得许多传统上必须在运行时完成的操作得以在编译时执行。其主要优势包括:实现零成本抽象、提升运行时性能、增强类型安全以及提供强大的元编程能力。
编译时与运行时上下文的区分
准确识别代码执行的上下文至关重要:
- 运行时上下文 (Runtime Context):
- 常规函数(未使用
comptime
标记的fn
)的函数体内部。 - 传递给常规函数的运行时参数的值。
- 常规函数(未使用
- 编译时上下文 (Comptime Context):
- 所有类型表达式 (
Type
) 和 Trait 表达式: 类型和 Trait 在 Flurry 中是一等公民,它们在编译时被定义、操作和求值。 - 所有属性 (
.
或^
): 附加到类型、函数、字段等的属性,其求值发生在编译时。 - 顶级命名空间 (Top-level Namespace) 中的定义: 全局作用域下的声明(如
const
,fn
,struct
定义)通常在编译时处理。 - 常量 (
const
) 的初始化表达式:const
声明要求其值必须在编译时确定。 comptime
函数的参数和函数体: 标记为comptime
的函数及其参数完全在编译时处理和执行。- 常规函数的
comptime
参数: 常规函数可以接收comptime
参数,这些参数的值在编译时确定并传入。 - 特定的语法结构: 某些语法结构,如
pattern_from_expr
中的<expr>
部分、结构体字段的默认值表达式等,要求其表达式必须是编译时可求值的。 inline
控制流结构的条件/控制部分: 对于inline if
,inline for
,inline when
等结构,其条件表达式或迭代控制逻辑在编译时求值。其内部代码块的执行上下文(编译时或运行时)则取决于inline
结构自身所处的上下文。
- 所有类型表达式 (
Flurry 结构:作为编译时值的语言构件
一个核心概念是:在 Flurry 的编译时环境中,许多语言的基本构件——例如结构体定义 (struct
)、函数定义 (fn
)、枚举定义 (enum
)、Trait 定义 (trait
) 等——本身被视为编译时值 (comptime values)。
这意味着这些定义不仅是声明,更是可以在编译时被操作和传递的数据。
示例:
const Student = struct {
name: String,
age: u8,
}
-- greet: pure comptime fn<T:- ToString> -> fn(T) -> String,
-- 可简写为 for<T:- ToString> fn(T) -> String
fn greet(name: T) -> String where T:- ToString {
"Hello, " + name.to_string()
}
Flurry 可能还提供更底层的编译时 API,用于以编程方式构建这些语言构件:
-- 使用假设的编译时 API 动态构建结构体类型
let DynamicStruct = Type.Struct.new(
.fields = { -- 一个编译时 object (异构映射)
.id { .type Uuid }, -- 字段名和类型
.value { .type str, .default "empty" }, -- 包含默认值
}
);
-- 在编译时对生成的类型进行内省 (Introspection)
let fields = Type.Struct.fields(DynamicStruct); -- 获取字段信息
assert(fields[1].get(.name, String)? == "value"); -- 安全地获取字段名
assert(fields[1].get(.default, String)? == "empty"); -- 安全地获取默认值
这种将语言构件视为编译时值的范式,使 comptime
超越了简单的常量折叠,成为一个强大的类型和代码生成引擎。
编译时类型系统的特性:依赖类型与渐进类型
Flurry 的编译时类型系统具备两个关键特性,增强了其表达能力和灵活性:
-
依赖类型 (Dependent Types): 函数(或其他接受参数的结构)的后续参数类型可以依赖于先前
comptime
参数的值。这允许创建高度灵活且类型安全的接口。println
函数是典型的例子,其可变参数varargs
的具体类型由comptime
参数format
字符串的值在编译时确定。-- varargs的类型是一个元组,从fmt.Parse(format)求值而出 fn println(comptime format: str, ...varargs: fmt.Parse(format)) { ... inline for v in varargs { const T = v'type; inline when { T: str => stdio.write_all(v), T:- Display => stdio.write_all(c.to_string().as_str()), T: usize => stdio.write_all(fmt.format_int(v)), ... } } }
-
渐进类型 (Gradual Typing): 编译时的容器类型,如
object
(异构键值对)和List<Any>
(异构列表),允许存储不同类型的值。Flurry 提供了渐进类型的访问机制来处理这种异构性:- 类型安全的获取 (
.get(key, ExpectedType: Type) -> ?ExpectedType
): 尝试获取指定键且类型为ExpectedType
的值。
let config = { .port 8080, .host "localhost", .enable_tls true } -- 编译时 object -- 类型安全获取 let port: ?i32 = config.get(.port, i32); assert(port? == 8080); let host: ?String = config.get(.host, String); assert(host? == "localhost"); let timeout: ?u64 = config.get(.timeout, u64); assert(timeout == null);
渐进类型为编译时处理结构未知或异构的数据提供了必要的灵活性。
- 类型安全的获取 (
编译时与运行时的交互机制
尽管编译时和运行时是不同的阶段,Flurry 提供了明确的机制允许它们交互:
-
沉降 (Lowering): 指将编译时可知的值作为常量嵌入到运行时代码中作为运行时值的过程。这通常是隐式发生的。例如,在运行时代码
let x = 10;
中,字面量10
是一个编译时整数值,被沉降为运行时的整数值。 -
comptime { ... }
块: 允许在运行时上下文中嵌入一段需要在编译时执行的代码。此代码块通常用于计算一个编译时值,该值随后可以通过沉降被外围的运行时代码使用。fn get_platform_info() -> String { -- os_name 在此被沉降为运行时字符串常量 let os_name: str = inline if build.target.os == .linux { "Linux" -- 编译时字符串 } else if build.target.os == .windows { "Windows" -- 编译时字符串 } else { "Unknown OS" -- 编译时字符串 } fmt.format("Running on: {}", os_name) }
-
inline
控制流 (inline if
/when
/for
/match
): 这些是关键的编译时控制流结构。它们根据编译时条件或数据,在编译阶段选择性地生成、省略或重复代码片段。inline if
/when
: 用于条件编译,根据编译时布尔表达式或类型匹配决定包含哪个代码分支。inline for
: 用于编译时循环展开(例如,遍历编译时已知的数组或元组)或根据编译时集合生成代码。
总结:comptime
的核心价值
Flurry 的 comptime
机制提供了一个功能完备的编译时编程环境,其核心特征包括:
- 两级计算模型: 清晰分离编译时与运行时。
- 语言构件作为编译时值: 类型、函数等可在编译时被操作。
- 依赖类型与渐进类型: 增强了编译时类型系统的表达力和灵活性。
- 编译时控制流 (
inline
): 实现代码生成和特化。 - 类型谓词: 提供强大的编译时约束能力。
通过 comptime
,Flurry 实现了:
- 零成本抽象: 高级编程构造在编译时被解析和优化,不引入运行时开销。
- 强大的元编程能力: 支持在编译阶段生成代码、构建类型、处理配置等。
- 增强的类型安全: 允许在编译时执行更深入的检查(例如,格式化字符串的类型安全验证)。
- 潜在的性能优化: 通过编译时特化、常量计算和代码生成,可以产出高度优化的运行时代码。
掌握 comptime
是充分利用 Flurry 语言潜力的关键,它为开发者提供了在编译阶段深度塑造程序结构和行为的能力。
类型谓词 —— 精确约束与静态断言 (Type Predicates: Precise Constraints and Static Assertions)
在深入探讨了 Flurry 强大的编译时计算能力之后,我们现在将目光投向其类型系统的另一个核心支柱:类型谓词 (Type Predicates)。类型谓词是可在编译时求值的逻辑命题,它们极大地增强了 Flurry 类型系统的表达能力,允许开发者对类型、值、编译时环境乃至程序行为施加精确的约束和静态断言。这些谓词是实现高级泛型、条件编译、编译时反射以及与形式化验证集成的基石。
类型谓词语法概览 (Type Predicate Grammar Overview)
Flurry 提供了一套丰富的语法来构造类型谓词,其核心构成要素包括但不限于:
- 类型归属 (Typing):
t : T
- 断言项
t
(通常是一个编译时表达式或变量)的类型为T
。
- 断言项
- Trait 约束 (Trait Bounds):
(t | T) :- A
- 断言项
t
或类型T
实现了 TraitA
。 - 支持复合 Trait 约束:
T :- A + B
:T
同时实现A
和B
。T :- ?A
:T
可能实现A
,因为有一些trait是默认实现的。T :- not A
:T
没有实现A
。
- 断言项
- 声明约束 (Declaration Bounds):
T :: t : U
或T :: static t : U
- 断言类型
T
内部包含一个名为t
的符号,其类型为U
。这用于访问类型的关联类型、常量、函数、全局变量等。
- 断言类型
- 成员约束 (Field/Method Bounds):
e :~ t : A
- 断言表达式
e
的类型具有一个名为t
的字段或方法,其类型签名符合A
(通常是一个函数类型或字段类型)。
- 断言表达式
- 子类型关系 (Subtyping):
T <: U
- 断言类型
T
是类型U
的子类型(如果 Flurry 支持结构化子类型或名义子类型)。
- 断言类型
- 类型匹配 (Type Matching):
T matches W
- 断言类型
T
的结构匹配模式W
。W
可以包含类型变量、通配符 (any
)、存在量化 (forall<...>
) 或其他类型构造,用于解构和匹配复杂类型结构。
- 断言类型
- 类型相等 (Type Equality):
T == U
- 断言类型
T
与类型U
等价。
- 断言类型
- 逻辑连接词 (Logical Connectives):
not p
: 逻辑非。p and q
: 逻辑与。p or q
: 逻辑或。p ==> q
: 逻辑蕴含。
这些谓词构件可以组合使用,形成复杂的编译时逻辑表达式,用于精确地描述对泛型参数、函数行为或编译环境的期望。
where
子句:约束的载体
类型谓词的一个重要应用场景是**where
子句**。where
子句可以附加到多种需要约束其参数或行为的语言结构上,例如:
- 泛型函数 (
fn ... where ...
) - 泛型结构体/枚举/Trait 定义 (
struct S where T
,enum E where T
,trait Tr where T
) impl
块 (impl Trait for Type where ...
)extend
块 (extend Trait for Type where ...
)derive
语句 (derive Trait for Type where ...
)
where
子句包含一个或多个由逗号分隔或逻辑连接词连接的类型谓词。
requires
与 ensures
:断言的角色
在 where
子句中,可以使用 requires
和 ensures
关键字来进一步明确谓词的角色(尽管简单的谓词可以直接写出):
requires p
: 表明谓词p
是一个前置条件 (Precondition) 或约束 (Constraint)。对于其所约束的结构(我们称之为受束者 (Subject),例如一个函数、一个impl
块或一个derive
),requires
谓词必须在编译时被满足,该受束者才被认为是可用的或有效的。ensures p
: 表明谓词p
是一个后置条件 (Postcondition) 或保证 (Guarantee)。它描述了如果满足前置条件,受束者成功完成后其状态或结果应该满足的属性。
requires
的核心作用:可用性控制
requires
谓词在 Flurry 的编译时系统中扮演着至关重要的角色,它直接决定了受束结构的可用性:
- 对于函数: 如果一个泛型函数的
where
子句包含requires P
,那么只有当为该函数提供的编译时参数(如类型参数)使得谓词P
为真时,该函数的这个特化版本才能被成功编译和调用。如果P
不满足,将导致编译时错误,指出约束不满足。 - 对于
impl
/extend
块: 如果impl Trait for Type where requires P
,那么只有当P
满足时,这个实现才会被编译器认为是有效的,并且Type
的实例才能通过该 Trait 进行方法调用。否则,编译器会认为该类型没有(或在此上下文中没有)实现该 Trait。这对于实现特化 (Specialization) 或条件实现 (Conditional Implementation) 非常关键。 - 对于
derive
语句: 如果derive Trait for Type where requires P
,那么只有当Type
满足P
时,编译器才会尝试为Type
自动派生Trait
的实现。这允许派生逻辑根据类型的特性进行调整。
示例:
-- 仅当 T 未实现 Debug 时,此 impl 才可用
impl Debug for Vec<T>
where requires not (T:- Debug)
{
-- ... 手动实现 ...
}
-- 仅当 T 实现了 Debug 时,此 derive 才生效
derive Debug for Vec<T>
where requires T:- Debug
-- 仅当编译时特性 "ast_dump_to_lisp" 启用时,此 impl 才可用
impl Display for Ast
where requires build.package.features.get("ast_dump_to_lisp", bool) matches true?
-- 仅当 T 是 String 或 str,或者 T 实现了 Display 时,此函数才可用
fn println(stream: S, data: T) -> !WriteErr void
where T, S:- io.Write,
requires T == String or T == str or T:- Display
{ {- ... -} }
在这些示例中,requires
子句充当了编译时的守卫 (Guard),确保只有在满足特定条件时,相关的代码或实现才会被启用或认为是合法的。这种机制使得 Flurry 能够进行高度精确和上下文相关的编译时决策。
类型谓词的应用场景
类型谓词的强大能力使其应用广泛:
- 高级泛型编程: 实现比简单 Trait 约束更复杂的泛型约束,例如要求类型具有特定的关联类型、静态成员或特定的结构(通过
matches
)。 - 条件编译: 基于类型属性、编译时配置 (
build.mode
,build.target
) 或特性标志 (build.package.features
) 来条件性地包含或排除代码、实现或派生。 - 特化 (Specialization): 为泛型结构提供基于类型谓词的特化实现,以获得更好的性能或针对特定类型提供不同的行为。
- 静态反射与元编程: 在
comptime
代码中查询类型的属性(例如,if T:- Copy { ... }
)并据此生成代码。 - 形式化规范与验证: 使用
requires
和ensures
来编写形式化的函数契约,并作为与后续形式化验证工具交互的基础。
未来展望:与类型推导和 Occurrence Typing 的结合
类型谓词系统与 Flurry 的其他高级特性(如类型推导和 Occurrence Typing)相结合,将带来更大的潜力。例如:
- 推导与检查: 类型谓词约束可以给类型提供更多信息,甚至是引入smt求解器来验证谓词的可满足性。编译器可以在编译时检查这些谓词,并在必要时生成错误或警告。
- Occurrence Typing: 在代码的不同分支中,编译器可能会根据控制流(例如,
inline if x is SomeType { ... }
)推断出变量更精确的类型或属性。这些推断出的信息可以用于在局部范围内满足更严格的类型谓词,从而允许调用在外部不可用的特化函数或实现。
这种结合将使得 Flurry 的静态分析能力更加强大,能够在编译时捕捉更细微的错误,并根据代码的实际执行路径启用更精确的优化和行为。
总结
Flurry 的类型谓词系统是其强大编译时能力和类型系统灵活性的重要体现。通过丰富的谓词语法和 where
子句(特别是 requires
),开发者可以对代码施加精确的编译时约束,实现高级泛型、条件编译、特化和静态断言。requires
谓词作为可用性的守卫,确保了代码在满足特定条件时才被启用。该系统不仅增强了代码的健壮性和表达力,也为未来与形式化验证的深度集成奠定了坚实的基础。理解和运用类型谓词,对于掌握 Flurry 的精髓至关重要。
代数效应 —— 结构化的控制流与副作用管理 (Algebraic Effects: Structured Control Flow and Effect Management)
在掌握了 Flurry 强大的编译时计算和精确的类型谓词之后,我们将探索 Flurry 用于处理程序动态行为——特别是副作用 (Side Effects) 和非局部控制流 (Non-local Control Flow)——的核心机制:代数效应 (Algebraic Effects)。代数效应提供了一种比传统异常处理、状态传递或 Monad 更具结构化、更灵活且更可组合的方式来管理程序的计算效应。
1. 概念定义:请求与处理的分离
让我们从核心概念开始。在 Flurry 中,可以将代数效应理解为一种请求-响应模型,但作用于函数调用栈之间:
- 请求 (Request / Effect Operation): 当一个被调用函数 (Callee) 执行到一个需要外部“帮助”或需要改变常规控制流的点时(例如,需要进行 I/O、读取/修改共享状态、抛出可恢复错误、实现协程等),它并不直接执行这个操作。相反,它发出 一个效应 (Effect) 调用。这个效应本质上是一个带有参数的“请求”,表明它需要某种特定的操作或决策。
- 处理 (Handling): 这个发出的效应会沿着调用栈向上传播,直到遇到一个为该效应安装的效应处理器 (Effect Handler)。
- 决策权转移: 处理器拦截这个效应,并根据效应的类型和参数决定如何响应。处理器拥有完全的控制权,它可以:
- 执行请求的操作(例如,实际执行 I/O)。
- 修改请求的参数。
- 向 Callee 返回一个值。
- 完全改变控制流(例如,不恢复 Callee 的执行,而是直接返回到更高层的调用者,类似异常)。
- 甚至多次恢复 Callee 的执行(用于实现协程、生成器等)。
- 关注点分离: 这种机制完美地实现了关注点分离。执行计算的函数 (Callee) 只需关心在何时需要何种效应(发出请求),而不必关心效应如何被实现。效应的具体实现(如何执行 I/O、如何管理状态、如何处理错误)则由调用者 (Caller) 通过安装不同的处理器来灵活地定义。
与传统方法的对比:
- 异常处理: 传统的异常处理(如 C++
throw
/catch
, Javatry
/catch
)只能单向地将控制权向上传递,并且通常难以恢复到抛出点。代数效应的处理器可以选择恢复 (resume) Callee 的执行。 - 状态传递/Monad: 手动传递状态或使用 Monad 会将副作用的处理逻辑侵入到函数签名和实现中,降低代码的直接性和可组合性。代数效应将效应处理逻辑与核心计算逻辑解耦。
2. 声明效应 (effect
)
要使用代数效应,首先需要声明它们。我们使用 effect
关键字来定义一个新的效应操作,它看起来有点像定义一个函数签名:
-- 声明一个名为 'ask' 的效应,它没有参数,期望返回一个 String
effect ask() -> String;
-- 声明一个名为 'output' 的效应,接收一个 String 参数,不返回值 (void)
effect output(message: String); -- -> void 是默认的
-- 声明一个泛型效应 'read_state',读取类型为 T 的状态
-- T 必须实现 Default trait (提供默认值)
effect read_state() -> T where T:- Default;
-- 声明一个可能失败的效应 'write_file'
-- 它接收路径和数据,返回一个结果,表示成功或一个 IoErr
effect write_file(path: String, data: Slice<u8>) -> !IoErr void;
effect <Name>
: 定义效应的名称。(...)
: 参数列表(可选),定义了发出该效应时需要携带的数据。-> ReturnType
: 返回类型(可选,默认为void
),定义了处理器恢复执行时应该提供给 Callee 的值的类型。where ...
: 泛型约束(可选)。
每个 effect
声明定义了一个新的效应操作签名。
3. 发出效应
fn user_interaction() -> #[output, ask] String {
-- 发出 output 效应,请求打印消息
output("What is your name?")#;
-- 发出 ask 效应,请求获取输入,并将处理器返回的值赋给 name
let name = ask()#;
"Hello, " + name
}
fn read_default_config() -> #[read_state<Config>] Config {
-- 发出泛型效应,读取 Config 类型的状态
let config = read_state<Config>()#;
config
}
4. 处理与恢复 (handles
, #{...}
, resume
)
调用者通过安装效应处理器 (Effect Handler) 来拦截和处理效应。
fn run_interaction() {
-- 调用 user_interaction,并为其安装一个处理器
let result = user_interaction()# {
-- 定义 ask 效应的处理分支
ask() => {
print("> "); -- 实际执行输出
let input = io.read_line(); -- 实际执行输入
input -- 恢复 user_interaction 的执行,并将 input 作为 ask() 的返回值
},
-- 定义 output 效应的处理分支
output(msg) => { -- 匹配 output(msg) 效应,并绑定参数 msg
println(msg); -- 实际执行输出
}
}
println("Final result: {}", result);
}
- 安装处理器:
expr# { ... }
语法用于安装处理器。expr
是发出效应的表达式,{ ... }
是处理器的定义。 - 处理器分支: 处理器对效应调用进行模式匹配。每个分支定义了如何处理特定的效应。
- 恢复执行:
resume
语句用于将处理器的结果返回给 Callee。通常,处理器会在分支中直接返回值(如input
),而不需要显式地使用resume
。但在某些情况下,可能需要使用resume
来明确恢复执行。 - 处理器的返回值: 处理器的返回值会被传递给发出效应的函数(Callee),作为该效应的返回值。
5. 代数效应的类型签名 (#EffectList Type
)
就像错误处理有 !Errors T
类型签名一样,代数效应也需要在函数签名中声明函数可能发出的效应。这有助于静态分析和保证效应被处理。
-
语法:
#EffectList Type
,其中EffectList
是一个编译时已知的效应列表(可能包含具体效应或泛型效应)。 -
示例:
-- 这个函数可能发出 ask 和 output 效应,并最终返回 String fn user_interaction() -> #[output, ask] String; fn read_default_config() -> #[read_state<T>] T where T:- Default; fn pure_add(a: i32, b: i32) -> #[] i32; -- 或者简写为 -> i32
-
效应检查: 编译器会检查函数体内部的效应调用是否与签名中声明的
EffectList
兼容。 -
Handler 消融规则 (Handler Elimination): 设效应处理器可处理的效应集为A, 计算的效应集为B,则将处理器应用与计算后,计算剩余的效应集为B - A。也就是说,处理器会消解掉它所处理的效应,使得最终的计算结果不再携带这些效应。这与错误处理的消减规则类似。
fn run_interaction() -> #[] void { -- run_interaction 本身不发出效应 -- user_interaction 的类型是 -> #[output, ask] String let result = user_interaction()# { -- 安装处理器 ask() => { ... }, output(msg) => { ... }, } println("Final result: {}", result); }
这种效应消解规则对于类型系统的健全性和模块化推理至关重要。
6. 为类型定义处理某些 Effect 的能力 (Prefabricated Handlers
)
Flurry 提供了一种独特的机制,允许将效应处理逻辑直接与某个类型关联起来,这被称为预制处理器 (Prefabricated Handlers)(我大概得取个好名字)。这使得该类型的实例能够“预制”处理特定效应的能力。
-- 假设我们有一个管理状态的 Runtime 类型
struct StateRuntime where T {
current_state: T,
}
impl StateRuntime<T> where T:- Clone {
handles write_state(new_state: T) {
self.current_state = new_state;
}
handles read_state() -> T {
self.current_state.clone()
}
}
--- 使用预制处理器 ---
fn use_runtime_state(runtime: StateRuntime<T>) -> T where T:- Clone + Default {
do {
let state = read_state()#; -- 发出效应
println("Read state: {any}", state);
state
}.use(runtime) -- 指示使用 runtime 的预制处理器
}
test {
let rt = StateRuntime<i32> { current_state: 10 }
let val = use_runtime_state(rt); -- val 会是 10
}
总结
代数效应是 Flurry 区别于许多传统系统级语言的关键特性,有望在并发/异步编程、状态管理、可测试性、DSL 构建等领域带来显著优势。理解代数效应的原理和机制,对于发挥 Flurry 的全部潜力至关重要。
宏系统
Flurry 不仅仅提供强大的编译时计算能力,还配备了一套层次丰富、功能强大的宏系统 (Macro System)。宏允许开发者在编译过程的不同阶段介入,对代码进行转换、生成和分析,极大地扩展了语言的表达能力和抽象能力。Flurry 的宏系统旨在提供从简单的文本替换到复杂的、基于语法的代码生成的全方位元编程支持。本章将详细介绍 Flurry 提供的五种主要宏类型。
1. 模板宏 (Template Macros)
概念:
模板宏是最基础的宏形式,其行为类似于 C/C++ 预处理器中的 #define
,主要用于执行简单的词法单元 (Token) 替换。它们在词法分析 (Lexical Analysis) 阶段早期生效,因此其操作对象是原始的 token 流,而非结构化的语法树。
特性:
- 生效阶段: 词法分析阶段。
- 输入/输出: 接收 token 或 token 序列,输出替换后的 token 序列。
- 作用域: 模板宏的定义仅在当前文件内有效,且必须在使用之前定义(词法顺序)。
- 定义方式: 使用
define
关键字。
语法形式:
Flurry 提供了几种 define
形式以适应不同的替换场景:
-
映射单个 Token 块:
define <macro_name>(<param_name>) { <replacement_tokens> }
将调用时括号内的 token 序列(作为一个整体,绑定到
<param_name>
)替换为<replacement_tokens>
。-- 定义一个宏,尝试将输入的 token 序列转换为整数 define get_int(tokens) { -- $tokens 代表调用时括号内的完整 token 序列 ($tokens).value.*.to_int()? } let js_object = ... ; -- 调用宏,js_object 被绑定到 tokens 参数 let value = get_int(js_object); -- 展开后: let value = (js_object).value.*.to_int()?;
-
映射固定数量的后续 Token:
define <macro_name> <N> { <replacement_tokens> }
将紧跟在宏名称后的
<N>
个 token 替换为<replacement_tokens>
。在替换体中,可以使用$1
,$2
, ...,$N
来引用捕获的第 n 个 token。-- 定义一个宏,用于生成 getter 方法 define getter 2 { -- 捕获后续 2 个 token -- $1 是第一个 token (方法名), $2 是第二个 token (字段名) fn $1(*self) { self.$2 } } struct Student { name: String, age: u32, -- 应用宏 $getter get_name name $getter get_age age -- 展开后: -- fn get_name(*self) { self.name } -- fn get_age(*self) { self.age } }
-
映射后续 Token 块 (按组):
define <macro_name> ...<N> { <replacement_tokens_per_group> }
将宏名称后花括号
{}
内的所有 token,按照每<N>
个一组进行分组,对每一组应用<replacement_tokens_per_group>
进行替换。在替换体中,同样使用$1
到$N
引用组内的 token。enum HttpStatus { .pattern_defined true, .base_type u32, -- 定义宏,每 2 个 token 为一组进行处理 define status ...2 { $1: $2, } -- $1 是状态名, $2 是状态码 -- 应用宏 $status { ok 200 not_found 404 internal_server_error 500 unauthorized 401 forbidden 403 } -- 展开后: -- ok: 200, -- not_found: 404, -- internal_server_error: 500, -- unauthorized: 401, -- forbidden: 403, }
适用场景: 模板宏适用于非常简单的、模式化的代码替换,例如定义常量别名、生成简单的重复代码结构(如 getter)或简化字面量列表的编写。由于它们在词法阶段工作,无法理解语法结构,因此不适用于复杂的代码转换。
2. 一类与二类构造宏 (Construction Macros: Type 1 & Type 2)
概念:
构造宏是 Flurry 宏系统中更强大的成员,它们在语法分析 (Parsing) 阶段生效。与模板宏处理原始 token 不同,构造宏能够理解和操作语法结构。它们由编译时 Flurry (comptime flurry
) 代码定义,在语法分析过程中被编译器调用执行,其输出会替换原来的宏调用点。Flurry 提供了两种主要的构造宏类型:
- 一类构造宏 (Macro Type 1): 直接操作 Flurry 抽象语法树 (AST)。
- 二类构造宏 (Macro Type 2): 使用 PEG 解析器定义自定义语法,然后操作该自定义语法树。
TokenBuffer
:宏的输入与输出媒介
在讨论构造宏之前,需要了解 TokenBuffer
。它是 Flurry 宏系统(特别是构造宏和后续类型)用于传递代码片段的核心数据结构,其本质是一个Token序列。
- 构造: 使用双花括号
{{ ... }}
语法构造一个TokenBuffer
。 - 插值: 可以在
{{ ... }}
内部使用$
符号插入实现了TryInto<meta.ast.TokenBuffer>
的值。这包括:- Flurry AST 节点 (来自
meta.ast
模块)。 - 用户通过 PEG 定义的新语法树节点。
- 另一个
TokenBuffer
。 编译器会自动处理将这些值“降级”或序列化回 token 流的过程。
- Flurry AST 节点 (来自
一类构造宏 (macro1
)
- 输入: Flurry 抽象语法树 (AST) 节点列表 (具体类型取决于宏调用的上下文和宏定义)。
- 处理: 由一个
comptime fn
实现,接收 AST 节点列表作为参数。 - 输出: 返回一个
TokenBuffer
。 - 定义: 使用
meta.macro1
宏定义一个一类构造宏。 - 调用: 通常使用
@macro_name(...)
或类似语法,括号内的内容会被解析为 Flurry AST 并传递给宏。
-- @macro_name(arg1, arg2, ...) 调用语法
const pattern_matches = macro1' {
-- 接收一个 Pattern 节点列表,每个节点代表一个 Flurry Pattern
fn(...patterns: Repetition<meta.ast.FlurryPattern>) -> TokenBuffer {
-- 使用 Flurry 的 comptime list 操作构建组合模式
let combined_pattern_ast = patterns.fold({{ not _ }}, |acc_ast, pattern_ast| {{ $acc or $pattern_ast }});
-- {{ $ast_node }} 会将 AST 节点序列化回 token
{{ $combined_pattern_ast => }} -- 生成 `pattern =>` 的 token 序列
}
}
test {
let word = "yes";
if word is {
-- @ 调用宏,"yes", "Y" 等会被解析为 Flurry String Literal Pattern AST
@pattern_matches("yes", "Y", "y", "\n") println("yes"),
-- 展开后: not _ or "yes" or "Y" or "y" or "\n" => println("yes"),
@pattern_matches("no", "N", "n") println("no"),
_ => println("unknown"),
}
}
二类构造宏 (macro2
)
- 输入: 宏调用点花括号
{}
内的原始TokenBuffer
。 - PEG 解析: 宏定义需要提供一个 PEG (Parsing Expression Grammar) 规则来解析输入的
TokenBuffer
。- PEG 简介: PEG 是一种形式化的语法表示方法,特别适合用于描述编程语言语法和构建解析器。与传统的上下文无关文法 (CFG) 不同,PEG 的选择操作符
/
是有序选择 (prioritized choice),它消除了文法的歧义性,使得解析过程更直接(通常是递归下降)。PEG 由一系列解析表达式 (Parsing Expressions) 组成,用于精确定义如何匹配和消耗输入序列。常见的 PEG 组合子包括:- 字面量 (
'text'
): 匹配精确的文本。 - 字符类 (
[a-z]
): 匹配范围内的字符。 - 序列 (
e1 e2
): 顺序匹配e1
和e2
。 - 有序选择 (
e1 / e2
): 优先尝试匹配e1
,如果失败则尝试e2
。 - 重复 (
e*
,e+
,e?
): 匹配零次或多次、一次或多次、零次或一次。 - 谓词 (
&e
,!e
): 检查是否能匹配e
(&e
,正向预测)或不能匹配e
(!e
,负向预测),但不消耗输入。
- 字面量 (
- PEG 简介: PEG 是一种形式化的语法表示方法,特别适合用于描述编程语言语法和构建解析器。与传统的上下文无关文法 (CFG) 不同,PEG 的选择操作符
- 处理: 宏定义的
comptime fn
接收由 PEG 解析输入TokenBuffer
后生成的自定义语法树作为参数。 - 输出: 返回一个
TokenBuffer
。 - 定义: 使用
' { peg_rule; fn(parsed_ast) -> TokenBuffer { ... } }
语法,并标记为macro2
。peg_rule
可以是内联定义的 PEG,也可以引用预定义的 PEG 规则(如 Flurry 内置的FlurryExtendedStatement
)。 - 调用: 使用
expr' { ... }
语法。花括号内的内容将作为TokenBuffer
传递给宏。
use meta.ast.*;
const time = macro2' {
-- 使用类型编码peg规则
-- newtype KPrint = Keyword<"print">,
-- newtype MyPrint = KPrint ~ FlurryExpression,
-- 常见的组合子有Alternative, Sequence, Repetition, Optional, `~`为Sequence的语法糖
-- 复用已有的语句定义,并且是拓展语句(不只是简单语句)
newtype Main = Repetition<FlurryExtendedStatement>,
fn(statements: Repetition<meta.ast.FlurryExtendedStatement>) -> TokenBuffer {
-- 可以在处理脚本中进行一些操作
-- 通过code template来插入语法树,自动根据插入的语法树类型lower到token buffer
{{
let __start = Duration.now();
$statements
let __end = Duration.now();
analyze(__start, __end);
}}
}
}
test `time macro` {
let x = 0;
-- 调用宏,花括号内的代码作为 TokenBuffer 输入
time' {
for i in 0..1000000 {
x += i;
}
println("Loop finished"); -- 可以包含多条语句
}
-- 展开后 (概念上):
-- {
-- let __start = std.time.Instant.now();
-- for i in 0..1000000 {
-- x += i;
-- }
-- println("Loop finished");
-- let __end = std.time.Instant.now();
-- std.debug.analyze_duration(__start, __end);
-- }
}
适用场景: 构造宏非常强大,适用于需要理解和转换代码结构的任务:
- 一类宏: 适用于你想直接操作标准 Flurry 语法结构的情况,例如分析或重组现有的 Flurry 代码模式。
- 二类宏: 极其适合创建嵌入式领域特定语言 (Embedded DSLs)。你可以定义自己的迷你语言语法(通过 PEG),然后将其无缝嵌入到 Flurry 代码中,宏负责将 DSL 解析并转换为底层的 Flurry 实现代码。
time' { ... }
、query_one' { ... }
都是二类宏的绝佳应用。
3. 三类构造宏 (macro3
)
- 输入: 宏调用点花括号
{}
内的原始TokenBuffer
。 - 处理: 由一个标记了
^macro3
的comptime fn
实现,该函数直接接收TokenBuffer
。它不使用 PEG 解析,而是直接对 token 流进行操作(遍历、替换等)。 - 输出: 返回一个处理后的
TokenBuffer
。
-- 定义一个三类宏,将输入 token 中的标识符转为大写
^macro3 -- 属性标记这是一个三类宏? 或者有其他机制?
comptime fn to_uppercase_macro(tokens: TokenBuffer) -> TokenBuffer {
tokens.foreach(|t| if t is .id(str) {
Token.id(str.to_uppercase()) -- 将标识符转换为大写
} else {
t
})
}
-- 调用宏
to_uppercase_macro' {
const message = "hello world"; -- message 会变成 MESSAGE
let variable_name = 1; -- variable_name 会变成 VARIABLE_NAME
}
-- 展开后 (概念上,只改标识符):
-- const MESSAGE = "hello world";
-- let VARIABLE_NAME = 1;
适用场景: 三类宏适用于需要对 token 流进行低级别操作的场景。
4. 四类构造宏 (macro4
)
- 输入: 宏调用点花括号
{}
内的原始源码字符串 (String),完全忽略 Flurry 的词法和语法规则。 - 处理: 由一个标记了
^macro4
的comptime fn
实现,接收String
作为输入。函数内部负责解析这个字符串(可以使用自定义解析器、外部工具等)。 - 输出: 返回一个
TokenBuffer
,该TokenBuffer
必须包含语法上有效的 Flurry 代码,后续会被 Flurry 编译器正常处理。
-- 定义一个四类宏,处理自定义的多行字符串格式
^macro4
comptime fn multi_line_string(input: String) -> TokenBuffer { ... }
let lyris = multi_line_string' {
say you, say me,
say it for always,
that s the way it should be.
}
适用场景: 四类宏是终极武器,用于处理那些完全不符合 Flurry 标准语法或词法规则的输入。它允许你:
- 完全实现自己的lexer、parser
- 嵌入对格式敏感(如缩进)的 DSL。
- 处理包含大量特殊字符、不需要 Flurry 转义的文本块(类似
builtin.raw_str
但带有自定义解析)。 - 直接从其他格式(如 CSV, JSON, YAML 等,如果需要内联且不想用运行时库)生成 Flurry 代码。
总结
Flurry 的宏系统提供了一个从简单到复杂的分层结构,允许开发者在编译过程的不同阶段、以不同的抽象层次(Token, Flurry AST, 自定义 AST, 原始字符串)进行元编程:
- 模板宏: 快速简单的词法替换。
- 一类构造宏: 基于 Flurry AST 的结构化转换。
- 二类构造宏: 基于 PEG 的自定义 DSL 解析和代码生成。
- 三类构造宏: 基于 TokenBuffer 的底层转换。
- 四类构造宏: 基于原始字符串的完全自定义解析和代码生成。
这套强大的宏系统,结合 Flurry 的 comptime
计算能力,使得开发者能够极大地提升代码的抽象层次、减少样板代码,并创建出富有表现力的嵌入式 DSL,是 Flurry 语言核心竞争力的重要组成部分。理解并恰当选用不同类型的宏,将是 Flurry 高级编程的关键。
错误处理 —— 可预见且可组合的健壮性 (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
)更原生的多错误组合能力。它使得编写健壮的系统级代码变得更加容易和可靠。