关于 moonbit-agent-guide
moonbit-agent-guide 面向编辑 MoonBit 项目的 Agent,提供项目布局、语言注意事项,以及导航、测试和交付检查指引。
主要功能
- 01MoonBit 模块与包布局指引
- 02使用 `moon ide doc` 进行语义导航
- 03使用 `.mbtx` 编写 Agent 自动化
- 04测试与验证流程
- 05语言及代码组织注意事项
适用场景
- 01编辑前了解 MoonBit 项目
- 02查找 API 并重构符号
- 03添加测试并验证改动
- 04使用 MoonBit 编写自动化脚本
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ moonbitlang/ skills --skill moonbit-agent-guide安装后在 Claude Code 中输入/moonbit-agent-guide确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitignoreide.mdLICENSEREADME.mdSKILL.mbt.mdreferences/advanced-moonbit-build.mdmoonbit-language-fundamentals.mbt.mdmoonbit-language-fundamentals.md详细介绍
MoonBit 任务清单
- 编辑前,先找到外层的
moon.mod和相关的moon.pkg文件。 - 使用
moon ide doc探索 API;使用outline、peek-def、find-references、hover和rename进行语义导航和重构。 - 仅使用
.mbtx编写由 Agent 编写的自动化代码;不要将自动化逻辑放入 shell 脚本或其他脚本语言中。 - 在正确的包内进行更改。文件不定义命名空间;使用
///|分隔顶层条目。 - 为 bug 修复添加回归测试。为新的公共 API 添加黑盒测试和文档注释示例。
- 使用
moon check和范围最小且相关的moon test验证;仅在有意更改快照时使用moon test --update。使用moon explain --diagnostic列出警告,并使用--warn-list启用选定的警告(例如,+unnecessary_annotation)。 - 交付前运行
moon fmt和moon info。检查生成的.mbti更改,尤其是在公共 API 应保持稳定时。
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 `options("is-main": true)`
├── 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)。
- 如果文件变得过大或职责不明确,就新建文件并将相关声明移过去。
-
你可以自由地在同一包内的文件之间移动声明。
- 每个代码块由
///|分隔。只要名称和 pub 属性保持不变,在文件之间移动函数/结构体/特征就不会改变语义。代码块的顺序也无关紧要。 - 在一个包内拆分或合并文件进行重构是安全的。
- 每个代码块由
-
文件名仅用于组织代码。
- 不要假设文件名定义了模块,也不要在类型路径中使用文件名。
- 文件名应描述功能或职责,而不是严格照搬类型名称。
-
添加新代码时:
- 优先添加到与该功能匹配的现有文件中。
- 如果没有合适的文件,就在同一包下创建一个名称能描述其职责的新文件。
- 避免创建庞大的“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 info生成,便于代码审查。当提交不改变公开 API 时,pkg.generated.mbti文件也会保持不变,因此建议在完成后将pkg.generated.mbti纳入版本控制。 不要直接修改pkg.generated.mbti,包括仅清理空白字符;应使用moon info重新生成,并检查其差异,将其作为公开 API 的变更信号。有关 IDE 导航和符号查找命令,请参阅下文专门介绍
moon ide的章节。
常见注意事项
- 变量和函数名称不要使用大写字母——否则会导致编译错误
- 可变记录字段别忘了加上
mut——字段默认不可变(请注意,数组通常不需要mut,除非要完全重新赋值给变量;例如,简单的推入操作就不需要mut) - 不要忽略错误处理——要么显式处理错误,要么在调用方声明
raise,让已检查错误向上传播 - 不要不必要地使用
return——最后一个表达式就是返回值 - 不要创建没有 Type:: 前缀的方法——方法需要显式的类型前缀
- 别忘了处理数组越界——使用
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 风格循环——for i in 0..<(n-1) {...}和for j in 0..=6 {...}是更符合 MoonBit 习惯的写法 - 不要使用
for { ... }编写无限循环——改用for ;; { ... } - 不要为了调试而
derive(Show)——派生Debug,并使用debug_inspect()输出测试或诊断信息(对组合值进行插值时使用\{Repr(value)})。仅在需要专门的显示格式(JSON、XML、领域文本)时,手动实现impl Show - 不要调用
@json.inspect()——使用不带包前缀的 prelude 函数json_inspect(value, ...) - 异步——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- 不构建,仅进行类型检查;请定期使用,速度很快 (moon check也支持--target和--diagnostic-limit <N>)moon info- 进行类型检查并生成mbti文件。 运行此命令,查看是否有公共接口发生变化。 (moon info也支持--target。)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 调用方的运行时叶节点开销”部分(该部分会将分配、引用计数和字符串相等性比较的开销映射回你自己的函数),同时生成 profile.json 和可在 Instruments 中打开的 .trace。在 macOS 上需要 Xcode 的 xcrun xctrace,因此请先安装完整的 Xcode(而不只是命令行工具)。单次解析或计算的时间太短,无法进行有意义的采样,因此应让经过分析的 main 循环执行热点路径数百次,并使用具有代表性的样例数据;这个循环测试框架是临时的,绝不应提交。
查看 自身耗时,了解哪个函数消耗了周期;查看 包含子调用的耗时,了解哪个调用子树占主导,然后快速迭代:分析、修复排名最高的问题、重新分析。相信差值前务必重新建立基准——采样耗时会随机器负载而波动,因此应交错对比该分支和 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[Int]) -> Int {
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],用于在整个项目中重命名符号。符号名称有歧义时,优先使用--loc。moon ide analyze [path],用于在规划安全重构时检查某个包或模块的公共 API 使用情况。 这些工具可以节省 token,而且比 grep 更精确(grep会显示定义和调用位置中的结果,包括注释)。
用 moon ide doc 发现 API
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? : Int) -> 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) -> Int?
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: Int, b: Int) -> Int {
+pub fn calculate_sum(a: Int, b: Int) -> Int {
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 示例
当用户询问:“第 14 行的 hover.mbt 中,filter 的签名和文档字符串是什么?”
$ moon ide hover filter --loc hover.mbt:14
test {
let a: Array[Int] = [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) -> UInt 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 : Int
| }
|
对于 --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 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"
options(
"is-main": true,
)
如果整个包仅支持选定的后端,可在顶层使用 supported_targets = "native" 或其他目标集表达式。
supported_targets = "native"
options(
"is-main": true,
)
包按目录划分,没有 moon.pkg 文件的目录不会被识别为包。
包导入(在 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或_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中设置is-main,并导入所需内容:import { "moonbitlang/async", "moonbitlang/async/stdio", } options( "is-main": true, ) - 定义
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传播出去。 - 被取消的任务不视为失败;它们会引发取消错误,但不会触发对同组任务的取消。
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 有垃圾回收机制,不需要生命周期注解,也没有所有权系统。
与 Rust 不同,MoonBit 类似 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(Int, Int) 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) -> Int 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 : Int, y : Int) -> Int 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) -> Int 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) -> Int {
let x = try! parse_int("123", position~) // label punning
x * 2
}
///|
/// Handle with try-catch
fn handle_parse(s : String, position~ : Position) -> Int {
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。
整数、Char 和重载字面量
MoonBit 支持 Byte、Int16、Int、UInt16、UInt、Int64、UInt64 等类型。
类型已知时,字面量可以重载:
///|
test "integer and char literal overloading disambiguation via type in the current context" {
let (int, uint, uint16, int64, byte) : (Int, UInt, 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) : (Int, 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')
}
字节(不可变)
///|
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")
}
}
数组(可调整大小)
///|
test "array literals overloading: disambiguation via type in the current context" {
let (a0, a1, a2, a3) : (
Array[Int],
FixedArray[Int],
ReadOnlyArray[Int],
ArrayView[Int],
) = ([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)
}
字符串(不可变 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 UInt 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, Int] = { "a": 1, "b": 2, "c": 3 }
let empty : Map[String, Int] = Map([]) // Empty map
// From array of pairs
let from_pairs : Map[String, Int] = 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 : Int = 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 或数组。反向转换则需要显式物化视图。
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[Int]) -> Int {
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 : Int
y : Int
} derive(Debug, ToJson) // derive traits for Point
///|
pub fn Point::Point(x~ : Int, y~ : Int) -> 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(Int)
InsertionPoint(Int)
} derive(Debug, Eq)
///|
pub fn binary_search(arr : ArrayView[Int], value : Int) -> 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[Int] = [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 循环,而不是命令式循环,因为它们更易阅读和理解。
标签参数和可选参数
正确示例:使用带标签的可选参数
///|
fn g(
positional : Int,
required~ : Int,
optional? : Int, // no default => Option
optional_with_default? : Int = 42, // default => plain Int
) -> String {
// These are the inferred types inside the function body.
let _ : Int = positional
let _ : Int = required
let _ : Int? = optional
let _ : Int = 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 : Int?, b : Int?, c : Int) -> 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? : Int = 1,而不是 b? : Int? = Some(1)。
///|
fn f_misuse(a? : Int?, b? : Int = 1) -> Unit {
let _ : Int?? = a // rarely intended
let _ : Int = b
}
// How to fix: declare `(a? : Int, b? : Int = 1)` directly.
///|
fn f_correct(a? : Int, b? : Int = 1) -> Unit {
let _ : Int? = a
let _ : Int = 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 : Int?
height : Int?
}
///|
fn not_idiomatic(opts : APIOptions, arg : Int) -> Int {
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 包组织指南
一个包应拥有用户预期会使用其构造函数、字段、模式匹配和方法的公开具体类型。所有者可以是门面包本身,也可以是门面有意重新导出的非内部公开包。
公开类型的所有权比实现位置更重要。如果用户将某个类型视为 @foo.X,那么 X 应定义在包 foo 中,或定义在由 foo 重新导出的公开包中;尤其是用户会调用 X::method、构造记录、匹配枚举构造器,或依赖生成的 .mbti 文档时。
当某个类型从非内部公开包重新导出时,MoonBit 可以在方法查找时隐式加载该类型的所有者包。例如,如果 @foo 从公开包 @bar 重新导出 type X,外部用户可以将值写作 @foo.X,并仍然调用由 @bar.X 提供的方法。
使用 internal/* 包提供实现支持:
- 扫描器
- 子语法解析器
- 转义/编码辅助工具
- 验证辅助工具
- 底层算法
- 私有辅助结果类型
不要将公开具体 API 类型放在 internal/* 中,并期望门面通过重新导出恢复完整 API。外部用户无法像对待内部包之外的类型那样,隐式加载内部包的方法所有者,因此即使 x 的类型是门面重新导出的类型,x.method() 也可能失败。这还会让构造函数、生成的接口和隐私边界更难理解。
正确使用 using
使用 pub using 提升门面的易用性,而不是定义类型所有权。
良好的用法:
// 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 的公开包时,这样做是合适的。
从内部包良好地重新导出值:
///|
pub using @impl {decode_entities}
如果导出函数的签名没有暴露内部类型,并且你有意将该值作为公开 API,那么这样做是可以接受的。
有风险的用法:
///|
pub using @internal_impl {type X}
避免对公开具体类型这样做。如果 X 是公开类型,应在门面包或非内部公开包中定义它。如果 X 确实是内部类型,就不要将其作为公开具体类型暴露。
需要实现以下目标时,请使用显式包装器,而不是 pub using:
- 将内部辅助结果转换为公开类型
- 强制使用公开默认值
- 隐藏内部辅助类型
- 明确公开 API 的所有权
- 让
.mbti更易于审查
实用规则
如果公开函数返回 X,并且用户应该检查、构造、模式匹配 X 或调用其方法,那么 X 就应属于用户所命名的门面包,或属于由门面重新导出的非内部公开包。
如果辅助包只为另一个包计算数据,它可以放在 internal/* 下,但其中的类型应保持内部,或仅作为简单的辅助结果类型,且不通过公开门面暴露。
良好的包边界如下:
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