关于 moonbit-agent-guide
面向 Agent 和开发者的 MoonBit 编码指南,涵盖项目结构、语言约定、测试和命令行工作流。指南强调使用 IDE 语义导航,并以 MoonBit 的 `.mbtx` 格式编写 Agent 自动化脚本。
主要功能
- 01说明 MoonBit 模块与包布局
- 02使用 `moon ide` 导航符号
- 03遵循语言与代码组织规则
- 04用 `.mbtx` 编写自动化
- 05验证、测试并格式化更改
适用场景
- 01熟悉不熟悉的 MoonBit 项目
- 02借助语义导航修改 API
- 03创建并验证 MoonBit 自动化脚本
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ moonbitlang/ moonbit-agent-guide --skill moonbit-agent-guide安装后在 Claude Code 中输入/moonbit-agent-guide确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitignoreide.mdLICENSEmoon.modmoon.pkgREADME.mdreferences/advanced-moonbit-build.mdmoon.pkgmoonbit-language-fundamentals.mbt.md详细介绍
MoonBit 任务清单
- 应用迁移建议前,检查
moon version --all和项目的依赖版本。本指南已使用 moon/moonc 2026-10-16(moonc v0.10.16)验证;下文有关异步取消的建议适用于moonbitlang/async0.22 及更高版本。 - 编辑前,找到所在的
moon.mod和相关的moon.pkg文件。 - 使用
moon ide doc探索 API;使用outline、peek-def、find-references、hover和rename进行语义导航与重构。 - Agent 编写的自动化代码只能使用
.mbtx;不要将自动化逻辑放在 shell 脚本或其他脚本语言中。 - 将更改限制在正确的包内。文件不定义命名空间;使用
///|分隔顶层条目。 - 为 bug 修复添加回归测试。为新增的公共 API 添加黑盒测试和文档字符串示例。
- 使用
moon check和范围最小的相关moon test进行验证;仅在有意更改快照时使用moon test --update。使用moon explain --diagnostic列出警告,并使用--warn-list启用选定的警告(例如,+unnecessary_annotation)。 - 交付前运行
moon fmt,并检查moon check生成的pkg.generated.mbti更改,尤其是在公共 API 应保持稳定时。 - 对于可执行文档,使用
--deny-warn检查并测试每个.mbt.md,不要一概抑制警告。示例也要格式化:兼容性语法可被接受,并不代表它就是规范语法。可以直接检查和测试独立 Markdown;如果moon fmt要求模块,则在临时模块中格式化副本,再将结果同步回来。
MoonBit 项目布局
MoonBit 使用 .mbt 作为源代码文件扩展名,使用 .mbti 作为接口文件扩展名。在 MoonBit 项目的顶层有一个 moon.mod 文件,用于指定项目的元数据。项目可以包含多个包,每个包都有自己的 moon.pkg。子目录中也可能包含 moon.mod 文件,表示该子目录可以使用一组不同的依赖。旧项目中可能仍包含 moon.mod.json;应将其视为旧版模块元数据格式,并将迁移或更新指南改为使用 moon.mod,而不是创建新的 moon.mod.json 文件。
布局示例
my_module
├── moon.mod # Module metadata; source option can specify the source directory
├── moon.pkg # Package metadata (each directory is a package like Golang)
├── README.mbt.md # Markdown with tested code blocks (`test "..." { ... }`)
├── README.md -> README.mbt.md
├── cmd # Command line directory
│ └── main
│ ├── main.mbt
│ └── moon.pkg # executable package with `pkgtype(kind: "executable")`
├── liba/ # Library packages
│ └── moon.pkg # Referenced by other packages as `@username/my_module/liba`
│ └── libb/ # Library packages
│ └── moon.pkg # Referenced by other packages as `@username/my_module/liba/libb`
├── user_pkg.mbt # Root packages, referenced by other packages as `@username/my_module`
├── user_pkg_wbtest.mbt # White-box tests (only needed for testing internal private members, similar to Golang's package mypackage)
└── user_pkg_test.mbt # Black-box tests
└── ... # More package files, symbols visible to current package (like Golang)
-
模块:由项目根目录中的
moon.mod文件标识。 MoonBit 模块类似于 Go 模块;它是由子目录中的多个包组成的集合,通常对应一个代码仓库或项目。 模块边界对依赖管理和导入路径很重要。 -
包:由每个目录中的
moon.pkg文件标识。 所有moon子命令仍会在模块所在目录(即存放moon.mod的目录)中执行,而不是在当前包目录中执行。 MoonBit 包是实际的编译单元(类似于 Go 包)。 同一包中的所有源文件会合并为一个单元,因此整个包共享所有定义。moon.mod文件中的name与包源代码目录的相对路径共同定义包名,而不是文件名。 导入引用的是模块路径与包路径,而绝不会引用文件名。 -
文件:
.mbt文件只是包内的一段源代码。 文件名不会创建模块、包或命名空间。 可以自由拆分、合并或移动同一包中的声明。 无论位于哪个文件,包中的任何声明都可以引用该包中的其他声明。
必须遵循的编码/布局规则:
-
优先使用多个小型、内聚的文件,而不是一个大型文件。
- 将相关类型和函数归入职责明确的文件(例如
http_client.mbt、router.mbt)。 - 如果文件变得过大或职责不清,请新建文件并将相关声明移过去。
- 将相关类型和函数归入职责明确的文件(例如
-
你可以自由地在同一包内的文件之间移动声明。
- 每个代码块由
///|分隔。在文件之间移动函数/结构体/特征不会改变语义,前提是名称和公开状态保持不变。代码块的顺序也无关紧要。 - 在包内拆分或合并文件进行重构是安全的。
- 每个代码块由
-
文件名仅用于组织。
- 不要假定文件名定义了模块,也不要在类型路径中使用文件名。
- 文件名应描述功能或职责,而不是严格照搬类型名称。
-
添加新代码时:
- 优先将代码添加到与该功能相符的现有文件中。
- 如果没有合适的文件,请在同一包下创建一个名称具有描述性的文件。
- 避免创建庞大的
impl、misc或util文件。
-
测试:
- 将测试放在相应包中的专用测试文件(例如
*_test.mbt)内。 对于一个包(除*_test.mbt文件外),*.mbt.md文件也是黑盒测试文件,同时也是 Markdown 文件。 由三个反引号分隔的代码块mbt check会被视为测试用例,同时用于文档和测试。 你可以使用包含mbt check代码示例的README.mbt.md文件。你也可以将README.mbt.md符号链接到README.md,以便更好地与 GitHub 集成。 - 使用多个小型测试文件完全没问题,也很值得鼓励。
- 将测试放在相应包中的专用测试文件(例如
-
接口文件(
pkg.generated.mbti)pkg.generated.mbti文件是编译器为每个包的公共 API 生成的摘要。 它们以正式、简洁的形式概述所有导出的类型、函数和特征,不包含实现细节。moon check默认会生成这些文件,它们对代码审查很有用。如果某次提交没有更改公共 API,pkg.generated.mbti文件就不会变化,因此建议在完成后将pkg.generated.mbti纳入版本控制。 不要直接修改pkg.generated.mbti,包括仅清理空白字符;请使用moon check重新生成,并审查其差异,以此确认公共 API 的变化。有关 IDE 导航和符号查找命令,请参阅下方专门介绍
moon ide的章节。
应避免的常见问题
- 变量/函数名不要使用大写字母——否则会导致编译错误
- 可变记录字段别忘了加上
mut——字段默认不可变(注意,通常无需为数组加上mut,除非要将数组整体重新赋值给变量。例如,简单的 push 操作不需要mut) - 不要忽略错误处理——要么显式处理错误,要么在调用方声明
raise,让已检查的错误向上传播 - 不必要时不要使用
return——最后一个表达式就是返回值 - 不要创建缺少 Type:: 前缀的方法——方法需要显式的类型前缀
- Trait 语法:在 trait 声明中使用
fn,并使用impl Trait for Type with fn method(...)。若要通过点语法公开 trait 实现,请显式使用extend Type with Trait::{method};参见references/moonbit-language-fundamentals.mbt.md中的 methods 示例。 - 别忘了处理数组越界情况——使用
get()安全访问 - 调用其他包中的函数时,别忘了加上 @package 前缀
- 不要使用 ++ 或 --(不支持)——请使用
i = i + 1或i += 1 - 不要为错误传播显式添加
try——在raise函数中,直接调用会引发错误的函数;使用catch在本地处理错误,仅在有意中止时使用try! - 旧版语法:旧代码可能会使用
function_name!(...)或function_name(...)?——这些语法已弃用;传播错误时请使用普通调用。 - 不要为
main写空参数列表——使用fn main { ... }或fn main raise { ... },不要使用fn main() { ... }或fn main() raise ... { ... } - 不要使用记录式枚举或错误构造器字段——带标签的构造器字段应写为
label~ : Type,例如InvalidNumber(input~ : String),而不是InvalidNumber(input: String) - 优先使用范围
for循环,而不是 C 风格循环——在 MoonBit 中,for i in 0..<(n-1) {...}和for j in 0..=6 {...}更符合惯用写法 - 无限循环不要写成
for { ... }——请改用for ;; { ... } - 不要为了调试而
derive(Show)——请派生Debug,并使用debug_inspect()输出测试/诊断信息(插值组合值时使用\{Repr(value)})。仅在需要专用显示格式(JSON、XML、领域文本)时手动实现impl Show - 不要调用
@json.inspect()——请使用前导模块中的json_inspect(value, ...),不要加包前缀 - JavaScript FFI:在 JavaScript 数组边界处使用
FixedArray[T]。可调整大小的Array[T]的 JavaScript 表示形式不是稳定的 FFI 契约。 - Async——MoonBit 没有
await关键字;不要添加该关键字。异步函数默认会引发错误,因此不要添加raise;只有在异步函数体不能引发错误时才添加noraise。 调用其他异步函数的函数和测试即为异步函数和测试。 要将函数或测试标记为异步,只需添加async前缀(例如[pub] async fn ...、async test ...)。
moon 基础知识
脚本模式(.mbtx)
仅将 .mbtx 用于由 Agent 编写的自动化任务。将循环、条件判断、解析、转换和进程编排放在 MoonBit 中,而不是 shell 脚本或其他脚本语言中。外层 shell 只应启动脚本或运行直接、单一用途的命令。
.mbtx 文件由可选的 import { ... } 块和普通 MoonBit 代码组成,其中包括 main 函数。直接运行:
moon run path/to/script.mbtx [args...]
根据脚本实际使用的效果选择 main:
fn main { ... }是同步的,不会传播错误。fn main raise { ... }是同步的,可能会传播受检错误。async fn main { ... }适用于调用异步 API 的脚本。导入"moonbitlang/async";不要添加raise或await。
这三种方式都适用于默认的 Wasm 目标。纯同步脚本不要标记为 async;MoonBit 会报告 unused_async。
自动化任务运行命令时,导入 "moonbitlang/async/shell" 并使用 @shell.Cmd 或 @shell.Pipeline。它们会将可执行文件与参数分开,并且绝不会调用 shell,因此 |、$() 和 * 等字符都会按原样传递。使用普通的 MoonBit 控制流,而不是 &&、shell 循环或命令替换。
脚本模式默认使用 Wasm。提供 --wasm-policy policy.json 可启用默认拒绝的 MoonBit 主机 API 控制;仅授予所需的文件系统、环境、网络或进程访问权限。对于进程自动化,优先使用范围较窄、指定确切程序和参数前缀的 process.allow 规则,而不是 process.spawn: true。该策略允许启动子进程,但不会对其自身进行沙盒隔离,因此如果子进程不受信任,主机也必须对其加以限制。
最简脚本(hello.mbtx):
fn main {
println("Hello from MoonBit script mode")
}
命令行自动化(sum_args.mbtx):
import {
"moonbitlang/core/env",
"moonbitlang/core/string",
}
fn main raise {
let args = @env.args()
for arg in args[1:]; total = 0 {
continue total + @string.parse_int(arg)
} nobreak {
println(total)
}
}
运行 moon run sum_args.mbtx 10 20 12 会输出 42。
可直接在脚本头部导入包(format_json.mbtx):
import {
"moonbitlang/core/json",
}
fn main raise {
let source =
#|{
#| "project": "moonbit",
#| "enabled": true
#|}
let value = @json.parse(source)
println(value.stringify(indent=2))
}
异步自动化适用于默认的 Wasm 目标。例如,运行 moon run async_job.mbtx,以执行:
import {
"moonbitlang/async",
}
async fn main {
@async.sleep(10)
println("async automation complete")
}
必备命令
moon new my_project- 创建新项目moon run cmd/main- 运行主包moon run - < hello.mbtx- 从标准输入运行脚本代码(适合快速试验)moon run -e "code snippet"- 从命令行参数运行代码(适合单行代码)moon run -e 'fn main { println("Hello, MoonBit!") }'moon build- 构建项目 (moon run和moon build均支持--target;moon build还支持--diagnostic-limit <N>)moon check- 不构建项目,执行类型检查,并默认生成pkg.generated.mbti文件。请定期运行,速度很快。 查看生成的文件,了解是否有任何公共接口发生变化。 (moon check还支持--target和--diagnostic-limit <N>)moon check --target all- 检查所有后端的类型 使用已保存的.mbtx脚本处理结构化诊断,而不是使用 shell 管道或其他脚本语言。例如,将以下内容保存为filter_diagnostics.mbtx;@shell.Cmd::each_line会直接运行moon check、 流式输出其 JSON 结果,并返回其退出状态:
import { "moonbitlang/async", "moonbitlang/async/shell", "moonbitlang/core/json", }
async fn main { let seen : Map[String, Unit] = Map([]) let exit_code = @shell.Cmd( "moon", ["check", "--target", "all", "--output-json"], ).each_line(line => { try @json.parse(line) catch { _ => () } noraise { { "level": "warning", "path": String(path), .. } => if !seen.contains(path) { seen[path] = () println(path) } _ => () } }) if exit_code != 0 { fail("moon check exited with code {exit_code}") } }
使用允许该命令前缀的策略运行它:
```json
{
"process": {
"allow": [
{
"program": "moon",
"args_prefix": ["check", "--target", "all", "--output-json"]
}
]
}
}
moon run --wasm-policy moon-check-policy.json filter_diagnostics.mbtx
moon explain- 显示编译器诊断和语言主题的内置文档。moon explain --diagnostic列出警告助记符和 ID。moon explain --diagnostic 31解释警告 31(unused_optional_argument)。moon explain --diagnostic unused_optional_argument通过助记符解释同一警告。moon explain --attribute列出支持的属性,例如#deprecated、#alias、#cfg、#coverage.skip和#warnings。moon explain --attribute deprecated解释#deprecated属性及其支持的形式。
moon add package- 添加依赖项moon remove package- 移除依赖项moon fmt- 格式化代码 - 应定期运行 - 请注意,文件可能会被重写 注意,也可以使用moon -C dir check在指定目录中运行命令。
热点路径分析(moon run --profile)
moon run --profile --target native --release cmd/<main> 会在采样分析器下运行原生发布版本,并打印按自身耗时和包含耗时排序的表格,以及“归因于 MoonBit 调用方的运行时叶节点开销”部分(该部分会将分配、引用计数和字符串相等性开销映射回你的函数),同时生成可在 Instruments 中打开的 profile.json 和 .trace。在 macOS 上需要 Xcode 的 xcrun xctrace,因此请先安装完整的 Xcode(而不只是命令行工具)。单次解析或计算的耗时太短,无法进行有效采样,因此应让经过性能分析的 main 运行一个循环,针对有代表性的样例执行热点路径数百次;这个循环测试框架是临时的,绝不应提交。
查看自身耗时,了解哪个函数消耗了 CPU 周期;查看包含耗时,了解哪个调用子树占主导,然后进行紧凑的循环:分析性能、修复排名第一的问题,再次分析。务必在相信性能差异前重新建立基线——采样计时会随机器负载波动,因此应交错构建并基准测试分支和 main,而不是拿它们与早前会话中的数字比较。
测试命令
moon test- 运行所有测试 (moon test也支持--target)moon test --update- 更新快照moon test -v- 输出详细信息及测试名称moon test [dirname|filename]- 测试指定目录或文件moon coverage analyze- 分析覆盖率moon test [dirname|filename] --filter 'glob'- 运行符合筛选条件的测试moon test float/float_test.mbt --filter "Float::*" moon test float -F "Float::*" // shortcut syntax
README.mbt.md 生成指南
- 在软件包目录中输出
README.mbt.md。*.mbt.md文件和文档注释内容会特殊处理mbt check。mbt check代码块会直接作为代码纳入,并由moon check和moon test运行。如果不想检查代码片段,建议显式使用mbt nocheck。 如果只引用软件包中的类型,应使用mbt nocheck,这样只会进行语法高亮。 将README.mbt.md符号链接到README.md,以适配要求使用README.md的系统。
测试指南
建议使用快照测试,因为行为发生变化时更新起来很方便。
-
快照测试:编写
inspect(value)/debug_inspect(value)/json_inspect(value),然后运行moon test --update(或moon test -u)来填充content=。- 对实现了
Show的值(基本类型或手动实现Show的类型)使用inspect()。 - 对派生了
Debug的任何类型使用debug_inspect()——这是自定义数据类型的默认选择。 - 对复杂的嵌套结构使用
json_inspect()(使用ToJsontrait,生成更易读的输出)。 - 如果函数的整个返回值不大,建议检查整个返回值,这样测试会更简单。根据需要为
YourType派生Debug和/或ToJson(或实现Show)。
- 对实现了
-
更新工作流:更改会影响输出的代码后,运行
moon test --update重新生成快照,然后检查测试文件中的差异(content=参数会自动更新)。 -
验证顺序:遵循
MoonBit Task Checklist。 -
默认使用黑盒测试:只通过
@package.fn调用公共 API。仅当私有成员很重要时才使用白盒测试。 -
分组:将相关检查合并到一个
test "..." { ... }块中,以提高速度和清晰度。 -
Panic:为测试命名时使用前缀
test "panic ..."{...};如果调用会返回值,则用ignore(...)包装以消除警告。 -
错误:预期成功时,直接调用会抛出错误的函数。如果调用意外抛出错误,测试会以实际错误失败。预期失败时,使用
try ... catch { ... } noraise { ... },在catch中检查错误,并在noraise中显式使测试失败。 预期失败的默认写法:try f() catch { err => inspect(err) } noraise { _ => fail("expected to fail") }。
文档注释测试
建议为公共 API 编写文档注释测试。
///|
/// Return the sum of an `Array`.
///
/// # Example
/// ```mbt check
/// test {
/// inspect(sum_array([1, 2, 3, 4, 5, 6]), content="21")
/// }
/// ```
pub fn sum_array(xs : Array[Int32]) -> Int32 {
xs.fold(init=0, (a, b) => a + b)
}
文档注释中的 MoonBit 代码会自动进行类型检查和测试(使用 moon test --update)。在文档注释中,mbt check 只能包含 test 或 async test。
规范驱动开发
- 规范可以写在只读的
spec.mbt文件中(这是惯用名称,但不是强制要求),并将存根代码标记为声明:
此处使用 mbt nocheck 展示了这个有意不完整的契约;它没有实现,不会作为本指南的一部分进行检查或运行。在项目中,提供实现之前,moon check 会报告未实现的声明。
///|
declare pub type Yaml
///|
declare pub fn Yaml::to_string(y : Yaml) -> String raise
///|
declare pub impl Eq for Yaml
///|
declare pub fn parse_yaml(s : String) -> Yaml raise
-
添加
spec_easy_test.mbt、spec_difficult_test.mbt等文件来测试规范函数;所有内容都会经过类型检查(moon check)。 -
得益于我们的软件包组织方式,AI 或用户可以在不同文件中实现
declare函数。 -
运行
moon test检查一切是否正确。 -
declare支持函数、方法和类型。 -
pub type Yaml这一行是有意设置的不透明占位符;其表示形式由实现者决定。 -
请注意,规范文件也可以包含普通代码,而不只是声明。
moon ide [doc|peek-def|outline|find-references|hover|rename|analyze] 用于代码导航和重构
对于项目本地的符号和导航,请使用:
- 使用
moon ide doc <query>查找 MoonBit 中可用的 API、函数、类型和方法。探索可用 API 时,始终优先使用moon ide doc,它比grep_search或任何基于正则表达式的搜索工具更强大、更准确。 - 使用
moon ide outline .扫描一个包, - 使用
moon ide find-references <symbol>查找用法,以及 - 使用
moon ide peek-def获取内联定义上下文并定位顶层符号。 - 使用
moon ide hover sym --loc filename:line:col获取特定位置的类型信息。 - 使用
moon ide rename <symbol> <new_name> [--loc filename:line:col]预览语义重命名。添加--apply可重写文件。符号名称有歧义时,优先使用--loc。 - 使用
moon ide analyze [path]在规划安全重构时检查包或模块的公共 API 用法。 这些工具可以节省 token,且比 grep 更精准(grep会同时显示定义和调用位置中的结果,包括注释)。
用于 API 探索的 moon ide doc
moon ide doc 使用专为符号查找设计的查询语法:
-
空查询:
moon ide doc ''- 在模块中:显示当前模块中所有可用的包,包括依赖项和 moonbitlang/core
- 在包中:显示当前包中的所有符号
- 在包外:显示所有可用的包
-
函数/值查找:
moon ide doc "[@pkg.]value_or_function_name" -
类型查找:
moon ide doc "[@pkg.]Type_name"(内置类型不需要包前缀) -
方法/字段查找:
moon ide doc "[@pkg.]Type_name::method_or_field_name" -
包探索:
moon ide doc "@pkg"- 显示包
pkg并列出其所有导出符号 - 示例:
moon ide doc "@json"——探索整个@json包 - 示例:
moon ide doc "@encoding/utf8"——探索嵌套包
- 显示包
-
多个查询:
moon ide doc "query1" "query2" ...- 在一次调用中运行多个查询并合并结果
- 示例:
moon ide doc "String" "Array" "@json",一次探索多个类型和一个包
-
通配匹配:使用
*通配符进行部分匹配,例如moon ide doc "String::*rev*"可查找名称中包含 "rev" 的所有 String 方法
moon ide doc 示例
# search for String methods in standard library:
$ moon ide doc "String"
type String
pub fn String::add(String, String) -> String
# ... more methods omitted ...
$ moon ide doc "@buffer" # list all symbols in package buffer:
moonbitlang/core/buffer
fn from_array(ArrayView[Byte]) -> Buffer
# ... omitted ...
$ moon ide doc "@buffer.new" # list the specific function in a package:
package "moonbitlang/core/buffer"
pub fn new(size_hint? : Int32) -> Buffer
Creates ... omitted ...
$ moon ide doc "String::*rev*" # globbing
package "moonbitlang/core/string"
pub fn String::rev(String) -> String
Returns ... omitted ...
# ... more
pub fn String::rev_find(String, StringView) -> Int32?
Returns ... omitted ...
最佳实践:将本节视为命令参考;验证要求定义在 MoonBit Task Checklist 中。
moon ide rename sym new_name [--loc filename:line:col] 示例
当用户询问:“可以将函数 compute_sum 重命名为 calculate_sum 吗?”
$ moon ide rename compute_sum calculate_sum --loc math_utils.mbt:2
*** Begin Patch
*** Update File: cmd/main/main.mbt
@@
///|
fn main {
- println(@math_utils.compute_sum(1, 2))
+ println(@math_utils.calculate_sum(1, 2))
}
*** Update File: math_utils.mbt
@@
///|
-pub fn compute_sum(a: Int32, b: Int32) -> Int32 {
+pub fn calculate_sum(a: Int32, b: Int32) -> Int32 {
a + b
}
*** Update File: math_utils_test.mbt
@@
///|
test {
- inspect(@math_utils.compute_sum(1, 2))
+ inspect(@math_utils.calculate_sum(1, 2))
}
*** End Patch
moon ide hover sym --loc filename:line:col 示例
当用户询问:“filter 的签名和文档字符串是什么?在 hover.mbt 第 14 行。”
$ moon ide hover filter --loc hover.mbt:14
test {
let a: Array[Int32] = [1]
inspect(a.filter((x) => {x > 1}))
^^^^^^
```moonbit
fn[T] Array::filter(self : Array[T], f : (T) -> Bool raise?) -> Array[T] raise?
```
---
Creates a new array containing all elements from the input array that satisfy
... omitted ...
}
moon ide peek-def sym [--loc filename:line:col] 示例
当用户询问:“可以检查一下 Parser::read_u32_leb128 的实现是否正确吗?”
你可以运行 moon ide peek-def Parser::read_u32_leb128 来获取定义上下文
(这比 grep 更好,因为它会基于语义搜索整个项目):
L45:|///|
L46:|fn Parser::read_u32_leb128(self : Parser) -> UInt32 raise ParseError {
L47:| ...
...:| }
现在,如果你想查看 Parser 结构体的定义,可以运行:
$ moon ide peek-def Parser --loc src/parse.mbt:46:4
Definition found at file src/parse.mbt
| ///|
2 | priv struct Parser {
| ^^^^^^
| bytes : Bytes
| mut pos : Int32
| }
|
对于 --loc 参数,行号必须准确;列号可以是近似值,因为位置参数 Parser 有助于定位。
如果“sym”是顶层符号,则可以省略位置:
$ moon ide peek-def String::rev
Found 1 symbols matching 'String::rev':
`pub fn String::rev` in package moonbitlang/core/builtin at /Users/usrname/.moon/lib/core/builtin/string_methods.mbt:1039-1044
1039 | ///|
| /// Returns a new string with the characters in reverse order. It respects
| /// Unicode characters and surrogate pairs but not grapheme clusters.
| pub fn String::rev(self : String) -> String {
| self[:].rev()
| }
用于包符号的 moon ide outline [dir|file] 和 moon ide find-references <sym>
使用 moon ide outline 扫描包或文件中的顶层符号,并查找其用法,无需使用 grep。
moon ide outline dir概览当前包目录(按文件显示标题)moon ide outline parser.mbt概览单个文件 当你需要快速了解一个包,或在使用peek-def前找到合适的文件时,这很有用。moon ide find-references TranslationUnit查找当前模块中对某个符号的所有引用
$ moon ide outline .
spec.mbt:
L003 | pub(all) enum CStandard {
...
L013 | pub(all) struct Position {
...
$ moon ide find-references TranslationUnit
包管理
发现模块和依赖项
moon search 'html markdown' --limit 5
moon view moonbitlang/async --versions
moon tree --json
moon tree --package --target native --json
search 和 view 查询注册表,可在项目之外使用。tree 检查当前项目解析后的依赖项;--package 显示包导入项,而不仅仅是模块依赖项。添加或升级前,先查明版本,而不是照搬旧示例中的版本。
添加依赖项
moon add moonbitlang/x # Add latest version
moon add moonbitlang/[email protected] # Add specific version
更新依赖项
moon update # Update package index
浏览第三方源代码(moon fetch)
moon fetch <author>/<module>[@<version>] 会将包的源代码下载到 .repos/<author>/<module>/<version>/,以便离线阅读(例如示例、内部实现、生成的 .mbti)。它不会将包添加到 moon.mod 中——要添加包,请使用 moon add。将 .repos/ 添加到 .gitignore。
moon fetch moonbitlang/[email protected] # browse source/examples without taking a dependency
常见的模块配置(moon.mod)
name = "username/hello"
version = "0.1.0"
readme = "README.mbt.md"
repository = ""
license = "Apache-2.0"
keywords = []
description = "..."
import {
"moonbitlang/[email protected]",
}
options(
// source: "src", // Optional; default is "."
"preferred-target": "native",
)
使用 moon add moonbitlang/[email protected] 和 moon remove moonbitlang/x 管理 import 块,而不是手动编辑依赖版本。
常见的包配置(moon.pkg)
简洁版 moon.pkg
import {
"username/hello/liba",
"moonbitlang/x/encoding" @libb,
}
import {
"username/hello/test_helpers",
} for "test"
import {
"username/hello/internal_test_helpers",
} for "wbtest"
pkgtype(kind: "executable")
如果整个包仅支持选定的后端,可在顶层使用 supported_targets = "native" 或其他目标集表达式。
supported_targets = "native"
pkgtype(kind: "executable")
每个目录对应一个包;没有 moon.pkg 文件的目录不会被识别为包。
每个包只能声明一个 pkgtype:library(默认值)、用于 main 入口点的 executable,或供外部宿主使用导出的 foreign_library。这些选项互斥。pkgtype(kind: "executable") 替代旧版 options("is-main": true);pkgtype(kind: "foreign_library") 替代旧版 options(link: true)。特定后端的 options(link: { ... }) 仍用于配置链接器详情;参见 references/advanced-moonbit-build.md。
包导入(用于 moon.pkg)
- 导入格式:
"module_name/package_path" - 用法:使用
@alias.function()调用导入的函数 - 默认别名:路径的最后一部分(例如,
username/hello/liba的别名为liba) - 包引用:在测试文件中使用
@packagename引用 被测试的包
包别名规则:
- 导入
"username/hello/liba"→ 使用@liba.function()(默认别名是路径的最后一段) - 使用自定义别名导入
import { "moonbitlang/x/encoding" @enc}→ 使用@enc.function()(注意:如果路径的最后一段与别名相同,则无需自定义别名。) - 在
_test.mbt中,显式使用被测试包的别名(@packagename.fn);不带限定的访问依赖兼容性导入,并会触发test_unqualified_package。_wbtest.mbt与包共享作用域,可直接访问私有定义。
示例:
///|
/// In main.mbt after importing "username/hello/liba" in `moon.pkg`
fn main {
println(@liba.hello()) // Calls hello() from liba package
}
使用标准库(moonbitlang/core)
moonbitlang/core 模块始终可用,无需将其添加到 moon.mod 依赖项中。普通的 core 包仍需在 moon.pkg 中显式导入,以使用 @utf8、@json 或 @strconv 等包别名;如果编译器报告缺少 core 包或隐式使用 core 包,请添加类似 "moonbitlang/core/encoding/utf8" 的导入项。
创建包
要在 . 下添加新包 fib:
-
创建目录:
./fib/ -
添加
./fib/moon.pkg -
添加包含代码的
.mbt文件 -
在依赖它的包中导入:
import { "username/hello/fib", }
有关 conditional compilation、link configuration、warning control 和 pre-build commands 等更高级的主题,请参见 references/advanced-moonbit-build.md。
异步 IO
异步编程使用编译器支持以及 moonbitlang/async
运行时。导入运行时后,async fn main 可在默认 Wasm 目标和原生目标上运行。各个主机 I/O API 仍可能因目标而异;请使用 moon ide doc "@async" 并在预期目标上验证。
面向用户的子包包括 @async(任务、计时器、取消)、
@async/aqueue、@async/fs、@async/shell(不依赖 shell 的进程编排)、
@async/stdio 和 @async/websocket。
每个子包都必须在 moon.pkg 或 .mbtx 导入块中单独导入。
- 使用
moon add moonbitlang/async添加依赖。 - 在可执行文件的
moon.pkg中声明包类型并导入所需内容:import { "moonbitlang/async", "moonbitlang/async/stdio", } pkgtype(kind: "executable") - 定义
async fn main(而非fn main)。通过with_task_group启动并发任务,以实现结构化并发:///| async fn main { @async.with_task_group(group => { group.spawn_bg(() => { @async.sleep(50) @stdio.stdout.write("A\n") }) group.spawn_bg(() => { @async.sleep(20) @stdio.stdout.write("B\n") }) }) }
- 异步函数默认具有可引发效应。请写成
async fn main { ... }或async fn f(...) { ... },而不是async fn main raise { ... }。 - 仅当异步函数体必须不引发错误时,才使用
async fn f(...) noraise { ... }。noraise异步函数不能调用可能失败的 API,除非在本地处理其错误。
with_task_group 的结构化并发约定:
with_task_group返回时,组内启动的所有任务都已终止。任务仍必须负责清理其获取的资源。- 如果任何已启动的任务失败(且启动时未指定
allow_failure=true),整个组都会失败:组内所有其他任务都会被取消,错误会从with_task_group传播出去。 - 被取消的任务不属于普通失败,也不会触发同组任务取消。在 async 0.22+ 中,取消是编译器信号,而非
suberror。
取消和清理(async 0.22+)
catch 用于处理错误,而非取消。使用 defer 确保在每种退出情况下都进行清理,使用 errdefer 在发生错误或取消时进行清理。仅用于清理的全捕获处理方式应迁移为上述写法。
noraise 并不意味着不可取消:@async.sleep 是 noraise,但仍可被取消。nocancel 是一种独立的效应;当清理必须在取消时仍完成,请使用 @async.protect_from_cancel。TaskGroup::add_defer 要求回调具有 nocancel 属性。
仅当需要显式处理取消时,才使用 @async.handle_cancellation。取消时它会返回 None,但不会重置任务的已取消状态。@async.is_cancellation_error 已弃用,且始终返回 false;with_cancellation_handler 已由 handle_cancellation 取代。请参阅 async 0.22 迁移说明。
spawn_bg / spawn 的闭包语法:
- ✅
() => { ... }— 符合惯用写法;异步属性会根据上下文推断。 - ✅
async fn() { ... }— 显式标注;等价于箭头形式。 - ⚠️
fn() { ... }— 会触发Warning [0027] deprecated_syntax:“thisfnis asynchronous but not annotated withasync”。请勿使用。 - ❌
async () => ...、fn() async { ... }、fn(args) async { ... }— 均会导致解析错误。async只能放在fn前,不能放在箭头 lambda 前或参数列表后。
异步测试
对于调用异步函数的测试,请使用 async test。包含该测试的包必须为测试模式导入 moonbitlang/async;测试中使用的任何异步子包也要在同一个 for "test" 代码块中导入。
import {
"moonbitlang/async",
"moonbitlang/async/stdio",
} for "test"
///|
async test "sleep completes" {
@async.sleep(1)
inspect("done", content="done")
}
- 没有
await关键字(类似于可能引发错误的函数)。在async test中,直接正常调用异步函数。 async test默认也具有异步引发错误的效果;不要添加raise。- 异步测试默认并行运行。除非每个测试都能隔离所用资源,否则请避免共享端口、文件、环境变量和全局可变状态。
- 在目标平台上运行
moon test;仅当被测 API 要求原生执行时,才添加--target native。检查测试名称或异步调度行为时,使用moon test -v。 - 在
README.mbt.md和文档字符串中,mbt check代码块可以包含async test代码块;请确保相应测试模式的包已导入moonbitlang/async。
MoonBit 语言导览
核心事实
- 表达式导向:
if、match和循环都会返回值;最后一个表达式就是返回值。 - 默认使用引用:数组/映射/结构体通过引用进行修改;需要修改基本类型时,请使用
Ref[T]。 - 代码块:使用
///|分隔顶层项。按代码块逐块生成代码。 如果希望代码块内部(由花括号括起)保留空行,请在空行后添加一行注释(可以带或不带注释文本)。 - 可见性:
fn默认是私有的;pub会在允许的范围内开放读取和构造;pub(all)允许外部构造。 - 命名约定:值/函数使用 lower_snake;类型/枚举使用 UpperCamel;枚举变体以 UpperCamel 开头。
- 包:代码文件中没有
import;通过@alias.fn调用。在moon.pkg中配置导入。 - 占位符:在 MoonBit 代码中,
...是表示未完成实现的有效占位符。 - 全局值:默认不可变,通常需要类型注解。
- 垃圾回收:MoonBit 使用 GC;没有生命周期注解,也没有所有权系统。
与 Rust 不同,但与 F# 类似,只有在需要重新赋值变量时才需要
let mut,修改结构体字段或数组/映射元素时则不需要。
MoonBit 错误处理(已检查错误)
MoonBit 使用已检查的错误引发函数,而不是未经检查的异常。所有错误都是 Error 的子类型;你可以使用 suberror 声明自己的错误类型。
已检查的错误会记录在函数签名中,而不是在每个调用点标记。可能引发错误的函数会声明 raise 或 raise SomeError。如果调用方只想向上传递该错误,也要声明兼容的 raise,并正常调用可能引发错误的函数。
- 在
raise函数中进行普通调用:错误会自动向上传递。 fn main raise { ... }可用于需要传递错误的同步命令行探测和小型示例。对于异步入口点,请使用async fn main { ... };异步函数默认可以引发错误。- 在
suberror构造器中,带标签的负载使用label~ : Type;调用和模式匹配时使用label=value。 - 使用
expr catch { ... }或try { ... } catch { ... }显式处理。 - 使用
try! expr:如果引发错误,则终止执行。
不要添加用于传递错误的 Swift 风格 try。新代码不要使用旧式 function_name!(...) 或 function_name(...)? 语法。
///|
/// Declare error types with 'suberror'
suberror ValueError {
ValueError(String)
}
///|
/// Tuple struct to hold position info
struct Position(Int32, Int32) derive(ToJson, Debug, Eq)
///|
/// ParseError is subtype of Error
pub(all) suberror ParseError {
InvalidChar(pos~ : Position, Char) // pos is labeled
InvalidEof(pos~ : Position)
InvalidNumber(pos~ : Position, String)
InvalidIdentEscape(pos~ : Position)
} derive(Eq, ToJson, Debug)
///|
/// Functions declare what they can throw
fn parse_int(s : String, position~ : Position) -> Int32 raise ParseError {
// 'raise' throws an error
if s is "" {
raise ParseError::InvalidEof(pos=position)
}
@string.parse_int(s) catch {
_ => raise ParseError::InvalidNumber(pos=position, s)
}
}
///|
/// Declare a specific error type when callers should handle it precisely
fn div(x : Int32, y : Int32) -> Int32 raise ValueError {
if y is 0 {
raise ValueError::ValueError("Division by zero")
}
x / y
}
///|
test "expected success calls directly" {
inspect(div(6, 3), content="2")
}
///|
test "expected failure handles the raised error" {
try div(1, 0) catch {
ValueError::ValueError(message) =>
inspect(message, content="Division by zero")
} noraise {
_ => fail("expected to fail")
}
}
// Three ways to handle errors:
///|
/// Propagate automatically
fn use_parse(s : String, position~ : Position) -> Int32 raise ParseError {
// This plain call is the correct propagation syntax.
// `try! parse_int(...)` would abort instead of propagating.
let x = parse_int(s, position~) // label punning, equivalent to position=position
// Error auto-propagates by default.
// Unlike Swift, you do not need to mark `try` for functions that can raise
// errors; the compiler infers it automatically. This keeps error handling
// explicit but concise.
x * 2
}
///|
/// Use try! to abort if it raises, no raise in the signature
fn use_parse2(position~ : Position) -> Int32 {
let x = try! parse_int("123", position~) // label punning
x * 2
}
///|
/// Handle with try-catch
fn handle_parse(s : String, position~ : Position) -> Int32 {
parse_int(s, position~) catch {
ParseError::InvalidEof(pos=_) => {
println("Parse failed: InvalidEof")
-1 // Default value
}
_ => 2
}
}
///|
test "error propagation and handling" {
let position = Position(0, 0)
inspect(use_parse("21", position~), content="42")
inspect(use_parse2(position~), content="246")
inspect(handle_parse("", position~), content="-1")
inspect(handle_parse("invalid", position~), content="2")
}
重要:调用可能引发错误的函数时,如果你只想向上传递错误,则无需添加任何标记;编译器会推断。异步函数默认可以引发错误,无需显式声明。不要为了传递错误而给异步函数添加 raise;只有当异步函数必须拒绝未处理的错误时,才添加 noraise。
使用 SubErrorType::_ as err 匹配某种错误类型的所有构造器,并将 Error 值缩窄为该子类型:
///|
fn is_parse_error(error : Error) -> Bool {
match error {
ParseError::_ as err => {
let _ : ParseError = err
true
}
_ => false
}
}
///|
test "matching an error subtype" {
assert_true(is_parse_error(ParseError::InvalidEof(pos=Position(0, 0))))
assert_false(is_parse_error(ValueError::ValueError("other")))
}
Integer、Char 和重载字面量
MoonBit 支持 Byte、Int16、Int32、UInt16、UInt32、Int64、UInt64 等类型。
在新示例中使用 Int32 和 UInt32。2026-10-16 版格式化程序会将仍受支持的别名 Int 和 UInt 规范化为这些名称。
已知类型时,字面量可以重载:
///|
test "integer and char literal overloading disambiguation via type in the current context" {
let (int, uint, uint16, int64, byte) : (Int32, UInt32, UInt16, Int64, Byte) = (
1, 1, 1, 1, 1,
)
// The literal `1` is overloaded based on the expected type in the current context.
// compile time error if the literal cannot be represented in the target type,
// e.g. let a7 : Byte = 256 // ❌ won't compile, 256 exceeds Byte max value 255
assert_eq(int, uint16.to_int())
assert_eq(uint, 1U)
assert_eq(int64, 1L)
assert_eq(byte, b'\x01')
let (a1, a2, a3) : (Int32, Char, UInt16) = ('b', 'b', 'b')
// char literal overloading, `a1` will be the unicode value of 'b',
// compile time error when the literal cannot be represented in the target type
// e.g, let a6 : UInt16 = '𐍈' // ❌ won't compile, '𐍈' is U+10348, which exceeds UInt16 max value 0xffff
let a4 : Byte = b'b' // Byte literal
assert_eq(a1, 98)
assert_eq(a2, 'b')
assert_eq(a3.to_int(), a1)
assert_eq(a4, b'b')
}
Bytes(不可变)
///|
test "bytes literals" {
let b0 : Bytes = b"abcd"
let b1 : Bytes = [0xff, 0x00, 0x01] // Array literal overloading
assert_eq(b1.length(), 3)
guard b0 is [b'a', ..] && b0[1] is b'b' else {
// Bytes can be pattern matched as BytesView and indexed
fail("unexpected bytes content")
}
}
Array(可调整大小)
///|
test "array literals overloading: disambiguation via type in the current context" {
let (a0, a1, a2, a3) : (
Array[Int32],
FixedArray[Int32],
ReadOnlyArray[Int32],
ArrayView[Int32],
) = ([1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3])
// The literal `[1, 2, 3]` is overloaded based on the expected type in the current context.
// Defaults to Array[_]
assert_eq(a0.length(), 3)
assert_eq(a1.length(), 3)
assert_eq(a2.length(), 3)
assert_eq(a3.length(), 3)
}
String(不可变 UTF-16)
s[i] 返回一个码元(UInt16),s.get_char(i) 返回 Char?。
由于 MoonBit 支持字符字面量重载,你可以编写如下代码片段:
///|
test "string indexing and utf8 encode/decode" {
let s = "hello world"
let b0 : UInt16 = s[0]
guard b0 is ('\n' | 'h' | 'b' | 'a'..<='z') && s is [.. "hello", .. rest] else {
fail("unexpected string content")
}
guard rest is " world" else { fail("unexpected string suffix") }
// In check mode (expression with explicit type), ('\n' : UInt16) is valid.
// Using get_char for Option handling
let b1 : Char? = s.get_char(0)
assert_true(b1 is Some('a'..<='z'))
// ⚠️ Important: Variables won't work with direct indexing
let eq_char : Char = '='
assert_false(s.get_char(0) == Some(eq_char))
// s[0] == eq_char // ❌ Won't compile - eq_char is not a literal, lhs is UInt16 while rhs is Char
// Use: s[0] == '=' or s.get_char(0) == Some(eq_char)
// Requires `"moonbitlang/core/encoding/utf8"` in `moon.pkg`.
let bytes = @utf8.encode("中文")
assert_true(bytes is [0xe4, 0xb8, 0xad, 0xe6, 0x96, 0x87])
let s2 : String = @utf8.decode(bytes) // decode utf8 bytes back to String
assert_true(s2 is "中文")
for c in "中文" {
let _ : Char = c // unicode safe iteration
println("char: \{c}") // iterate over chars
}
}
字符串插值 && StringBuilder
MoonBit 使用 \{} 进行字符串插值;对于自定义类型,它们需要实现 trait Show。
///|
test "string interpolation basics" {
let name : String = "Moon"
let config = { "cache": 123 }
let version = 1.0
println("Hello \{name} v\{version}") // "Hello Moon v1"
// ✅ Quoted map keys are allowed inside interpolation expressions.
println(" - Checking if 'cache' section exists: \{config["cache"]}")
let sb = StringBuilder()
sb <+ "[\{[ for x in [1, 2, 3] => "\{x}" ].join(",")}]"
inspect(sb, content="[1,2,3]")
let x = 42
let streamed = StringBuilder()
streamed <+ "hello \{x}"
inspect(streamed, content="hello 42")
}
\{} 内的表达式必须是单行表达式。
支持嵌套插值和字符串字面量,但 \{} 内不能换行。
用于流式插值的 <+ 和 <? 宏
字符串插值可以通过 <+ 直接流式写入 Logger/StringBuilder 风格的写入器,也可以通过 <? 有条件地写入可选写入器:
writer <+ "hello \{x}"
writer <+ {"key1": value, "key2": value2}
lhs <? "hello \{x}"
lhs <? {"key1": value, "key2": value2}
这会展开为对写入器的调用:
writer.write_string("hello ")
writer.write(x)
writer.write_object_begin()
writer.write_object_field("key1", value)
writer.write_object_field("key2", value2)
writer.write_object_end()
if lhs is Some(l) { l <+ "hello \{x}" }
字面量字符串片段使用 write_string;插值表达式使用 write。
对于 <?,None 不执行写入;Some(writer) 则对包装的写入器应用相同的 <+ 展开。
<+ 和 <? 的右侧必须是模板字符串/多行模板字符串或映射对象字面量,不能是任意表达式。
展开采用宏风格:它取决于 writer 类型如何为模板字符串实现 write_string 和 write,以及如何为映射对象字面量实现 write_object_begin、write_object_field 和 write_object_end。HTMLBuilder 或 JSONBuilder 等类型可以使用相同语法支持插值和流式写入,但语义不同。
由于 MoonBit 允许在外部类型上定义局部方法,包可以通过添加这些局部写入器方法,让现有写入器类型适配此语法。
多行字符串
///|
test "multi-line string literals" {
let multi_line_string : String =
#|Hello "world"
#|World
#|
let multi_line_string_with_interp : String =
$|Line 1 ""
$|Line 2 \{1+2}
$|
// no escape in `#|`,
// only escape '\{..}` in `$|`
assert_eq(multi_line_string, "Hello \"world\"\nWorld\n")
assert_eq(multi_line_string_with_interp, "Line 1 \"\"\nLine 2 3\n")
}
Map(可变,保留插入顺序)
///|
test "map literals and common operations" {
// Map literal syntax
let map : Map[String, Int32] = { "a": 1, "b": 2, "c": 3 }
let empty : Map[String, Int32] = Map([]) // Empty map
// From array of pairs
let from_pairs : Map[String, Int32] = Map::from_array([("x", 1), ("y", 2)])
assert_true(empty.is_empty())
inspect(from_pairs["x"], content="1")
// Set/update value
map["new-key"] = 3
map["a"] = 10 // Updates existing key
// Get value - returns Option[T]
guard map is { "new-key": 3, "missing"? : None, .. } else {
fail("unexpected map contents")
}
// Direct access (panics if key missing)
let value : Int32 = map["a"] // value = 10
inspect(value, content="10")
// Iteration preserves insertion order
for k, v in map {
println("\{k}: \{v}") // Prints: a: 10, b: 2, c: 3, new-key: 3
}
// Other common operations
map.remove("b")
guard map is { "a": 10, "c": 3, "new-key": 3, .. } && map.length() == 3 else {
// "b" is gone, only 3 elements left
fail("unexpected map contents after removal")
}
}
视图类型
核心概念:视图类型(StringView、BytesView、ArrayView[T])可在不复制底层数据的情况下,只读访问一个范围。接受视图的参数也可以通过隐式转换接受相应的 String、Bytes 或数组。反向转换则需要显式物化视图。
数组和字节切片也会钳制范围:[10, 20, 30][1:100] 包含 20, 30,而起始位置超过末尾的范围为空。若无效范围应触发 panic,请使用 exact_view;若应返回 None,请使用 get_view。
String→StringView:使用s[:]、s[start:end]、s[start:]或s[:end]Bytes→BytesView:使用b[:]或b[start:end]等Array[T]、FixedArray[T]、ReadOnlyArray[T]→ArrayView[T]:使用a[:]或a[start:end]等
字符串边界:偏移量按 UTF-16 码元计数。根据对无效边界的处理方式选择操作:
s[a:b](clamped_view)会钳制超出范围的偏移量,并将边界向内调整,以避免拆分代理对。它不会抛出异常或触发 panic;反向范围会生成空视图。s.exact_view(start=a, end=b)要求边界精确且有效,否则触发 panic。s.get_view(start=a, end=b)在边界无效时返回None。
///|
test "string view boundaries and ownership" {
let s = "ab😀cd"
inspect(s[0:3], content="ab")
inspect(s[3:6], content="cd")
inspect(s[0:100], content="ab😀cd")
inspect(s[4:2], content="")
assert_true(s.get_view(end=3) is None)
let owned : String = s.exact_view(start=2, end=4).to_owned()
inspect(owned, content="😀")
}
何时使用视图:
- 使用剩余模式(
[first, .. rest])进行模式匹配 - 将切片传递给函数,避免分配开销
- 避免不必要地复制大型序列
使用 .to_owned() 可将 StringView、BytesView 或 ArrayView[T] 物化为 String、Bytes 或 Array[T]。旧的视图转换方法 .to_string()、.to_bytes() 和 .to_array() 已弃用。(moon ide doc StringView)
用户定义类型(enum、struct)
///|
enum Tree[T] {
Leaf(T) // Unlike Rust, no comma here
Node(left~ : Tree[T], T, right~ : Tree[T]) // enum can use labels
} derive(Debug, ToJson) // derive traits for Tree
///|
pub fn Tree::sum(tree : Tree[Int32]) -> Int32 {
match tree {
Leaf(x) => x
// we don't need to write Tree::Leaf, when `tree` has a known type
Node(left~, x, right~) => left.sum() + x + right.sum() // method invoked in dot notation
}
}
///|
struct Point {
x : Int32
y : Int32
} derive(Debug, ToJson) // derive traits for Point
///|
pub fn Point::Point(x~ : Int32, y~ : Int32) -> Point {
{ x, y, }
}
///|
test "user defined types: enum and struct" {
let tree = Node(left=Leaf(1), 2, right=Leaf(3))
inspect(tree.sum(), content="6")
json_inspect(Point(x=10, y=20), content={ "x": 10, "y": 20 })
debug_inspect(
Point(x=10, y=20),
content=(
#|{ x: 10, y: 20 }
),
)
}
函数式 for 循环
///|
pub(all) enum SearchIndex {
Found(Int32)
InsertionPoint(Int32)
} derive(Debug, Eq)
///|
pub fn binary_search(arr : ArrayView[Int32], value : Int32) -> SearchIndex {
let len = arr.length()
// functional for loop:
// initial state ; [predicate] ; [post-update] {
// loop body with `continue` to update state
//} nobreak { // exit block
// }
// predicate and post-update are optional
for i = 0, j = len; i < j; {
// post-update is omitted, we use `continue` to update state
let h = i + (j - i) / 2
if arr[h] < value {
continue h + 1, j // functional update of loop state
} else {
continue i, h // functional update of loop state
}
} nobreak { // exit of for loop
if i < len && arr[i] == value {
Found(i)
} else {
InsertionPoint(i)
}
}
}
///|
test "functional for loop control flow" {
let arr : Array[Int32] = [1, 3, 5, 7, 9]
debug_inspect(binary_search(arr, 5), content="Found(2)") // Array to ArrayView implicit conversion when passing as arguments
debug_inspect(binary_search(arr, 6), content="InsertionPoint(3)")
// for iteration is supported too
for i, v in arr {
println("\{i}: \{v}") // `i` is index, `v` is value
}
}
强烈建议 尽可能使用函数式 for 循环,而不是命令式循环,因为前者更易于阅读和推理。
标签参数和可选参数
良好示例:使用带标签的参数和可选参数
使用 label~ : T 表示必需的标签参数,使用 label~ : T = default 表示带默认值的标签参数,使用 label? : T 表示省略时以 T? 表示的参数。格式化工具会将旧式的带默认值写法 label? : T = default 改写为 label~ : T = default。
///|
fn g(
positional : Int32,
required~ : Int32,
optional? : Int32, // no default => Option
optional_with_default~ : Int32 = 42, // default => plain Int32
) -> String {
// These are the inferred types inside the function body.
let _ : Int32 = positional
let _ : Int32 = required
let _ : Int32? = optional
let _ : Int32 = optional_with_default
// `Repr` renders Option without relying on its deprecated `Show` implementation.
"\{positional},\{required},\{Repr(optional)},\{optional_with_default}"
}
///|
test {
inspect(g(1, required=2), content="1,2,None,42")
inspect(g(1, required=2, optional=3), content="1,2,Some(3),42")
inspect(g(1, required=4, optional_with_default=100), content="1,4,None,100")
}
错误用法:arg : Type? 不是可选参数。
调用方仍必须传入它(以 None/Some(...) 的形式)。
///|
fn with_config(a : Int32?, b : Int32?, c : Int32) -> String {
"\{Repr(a)},\{Repr(b)},\{c}"
}
///|
test {
inspect(with_config(None, None, 1), content="None,None,1")
inspect(with_config(Some(5), Some(5), 1), content="Some(5),Some(5),1")
}
反模式:arg? : Type?(没有默认值 ⇒ 双重 Option)。
如果你想要带默认值的可选参数,请写 b~ : Int32 = 1,而不是 b~ : Int32? = Some(1)。
///|
fn f_misuse(a? : Int32?, b~ : Int32 = 1) -> Unit {
let _ : Int32?? = a // rarely intended
let _ : Int32 = b
}
// How to fix: declare `(a? : Int32, b~ : Int32 = 1)` directly.
///|
fn f_correct(a? : Int32, b~ : Int32 = 1) -> Unit {
let _ : Int32? = a
let _ : Int32 = b
}
///|
test {
f_misuse(b=3)
f_misuse(a=Some(5), b=2) // works but confusing
f_correct(b=2)
f_correct(a=5)
}
错误示例:arg : APIOptions(应改用带标签的可选参数)
///|
/// Do not use struct to group options.
struct APIOptions {
width : Int32?
height : Int32?
}
///|
fn not_idiomatic(opts : APIOptions, arg : Int32) -> Int32 {
opts.width.unwrap_or(arg) * opts.height.unwrap_or(arg)
}
///|
test {
// Hard to use in call site
inspect(not_idiomatic({ width: Some(5), height: None, }, 10), content="50")
inspect(not_idiomatic({ width: None, height: None, }, 10), content="100")
}
MoonBit 包组织指南
一个包应拥有用户预期会使用其构造器、字段、模式匹配和方法的公共具体类型。所有者可以是 facade 包本身,也可以是 facade 有意重新导出的非 internal 公共包。
公共类型的所有权比实现位置更重要。如果用户将某个类型视为 @foo.X,那么 X 应定义在 foo 包中,或定义在由 foo 重新导出的公共包中;尤其当用户会调用 X::method、构造记录、匹配枚举构造器,或依赖生成的 .mbti 文档时更是如此。
当从非 internal 公共包重新导出类型时,MoonBit 可以为方法查找隐式加载该类型的所有者包。例如,如果 @foo 从公共包 @bar 重新导出 type X,外部用户可以将值写作 @foo.X,同时仍能调用由 @bar.X 所有的方法。
使用 internal/* 包来提供实现支持:
- 扫描器
- 子语法解析器
- 转义/编码辅助函数
- 校验辅助函数
- 底层算法
- 私有辅助结果类型
不要将公共具体 API 类型放在 internal/* 中,并指望 facade 通过重新导出恢复完整 API。外部用户无法像对待 internal 包中的类型那样,隐式加载其方法所有者,因此即使 x 的类型是 facade 重新导出的类型,x.method() 也可能失败。这也会让构造器、生成的接口和隐私边界更难以推理。
正确使用 using
使用 pub using 提升 facade 的易用性,而不是用于确定类型所有权。
良好用法:
// root package
///|
pub using @parser {parse, parse_fragment}
///|
pub using @dom {type Node, type NodeKind, to_markdown}
///|
pub using @serializer {type HtmlContext}
当 @parser、@dom 和 @serializer 是已经拥有这些 API 的公共包时,这样做是合适的。
从 internal 包良好地重新导出值:
///|
pub using @impl {decode_entities}
如果导出函数的签名没有暴露 internal 类型,并且你有意将该值作为公共 API,那么这样做是可以接受的。
有风险的用法:
///|
pub using @internal_impl {type X}
避免对公共具体类型这样做。如果 X 是公共类型,请在 facade 包或非 internal 公共包中定义它。如果 X 确实是 internal 类型,就不要将其作为公共具体类型暴露。
在需要以下操作时,请使用显式包装器,而不是 pub using:
- 将 internal 辅助结果转换为公共类型
- 强制采用公共默认值
- 隐藏 internal 辅助类型
- 明确公共 API 的所有权
- 让
.mbti更易于审查
实用规则
如果公共函数返回 X,并且用户应当能够检查、构造、进行模式匹配或调用 X 的方法,那么 X 应属于用户所使用的 facade 包,或属于由 facade 重新导出的非 internal 公共包。
如果某个辅助包只为另一个包计算数据,它可以放在 internal/* 下,但其中的类型应保持 internal,或仅作为不通过公共 facade 暴露的简单辅助结果类型。
良好的包边界如下:
foo/
types.mbt // public Foo, FooMode, FooResult
api.mbt // public functions and Foo::methods
private_impl.mbt // private implementation files in same package
internal/foo/
scanner.mbt
escaping.mbt
validation.mbt