mcpField
M
moonbit-agent-guide

moonbit-agent-guide

github.com/moonbitlang开发34277 次安装6 周前更新
代码仓库

关于 moonbit-agent-guide

moonbit-agent-guide 面向编辑 MoonBit 项目的 Agent,提供项目布局、语言注意事项,以及导航、测试和交付检查指引。

主要功能

  1. 01MoonBit 模块与包布局指引
  2. 02使用 `moon ide doc` 进行语义导航
  3. 03使用 `.mbtx` 编写 Agent 自动化
  4. 04测试与验证流程
  5. 05语言及代码组织注意事项

适用场景

  1. 01编辑前了解 MoonBit 项目
  2. 02查找 API 并重构符号
  3. 03添加测试并验证改动
  4. 04使用 MoonBit 编写自动化脚本

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/moonbitlang/skills --skill moonbit-agent-guide

安装后在 Claude Code 中输入/moonbit-agent-guide确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.gitignore
ide.md
LICENSE
README.md
SKILL.mbt.md
references/
advanced-moonbit-build.md
moonbit-language-fundamentals.mbt.md
moonbit-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 文件只是包中的一段源代码。 文件名不会创建模块、包或命名空间。 你可以自由地在同一包的文件之间拆分、合并和移动声明。 无论文件如何,包中的任何声明都可以引用该包中的其他声明。

你必须遵守的编码/布局规则:

  1. 优先采用多个小而内聚的文件,而不是一个大文件。

    • 将相关类型和函数归入职责明确的文件(例如 http_client.mbt、router.mbt)。
    • 如果文件变得过大或职责不明确,就新建文件并将相关声明移过去。
  2. 你可以自由地在同一包内的文件之间移动声明。

    • 每个代码块由 ///| 分隔。只要名称和 pub 属性保持不变,在文件之间移动函数/结构体/特征就不会改变语义。代码块的顺序也无关紧要。
    • 在一个包内拆分或合并文件进行重构是安全的。
  3. 文件名仅用于组织代码。

    • 不要假设文件名定义了模块,也不要在类型路径中使用文件名。
    • 文件名应描述功能或职责,而不是严格照搬类型名称。
  4. 添加新代码时:

    • 优先添加到与该功能匹配的现有文件中。
    • 如果没有合适的文件,就在同一包下创建一个名称能描述其职责的新文件。
    • 避免创建庞大的“impl”“misc”或“util”文件。
  5. 测试:

    • 将测试放在相应包内专用的测试文件中(例如 *_test.mbt)。 对于一个包(除 *_test.mbt 文件外),*.mbt.md 文件除了 Markdown 文件之外,也属于黑盒测试文件。 以三个反引号分隔的代码块 mbt check 会被视为测试用例,兼具文档和测试的作用。 你可以使用包含 mbt check 代码示例的 README.mbt.md 文件。你也可以将 README.mbt.md 符号链接到 README.md, 以便更好地与 GitHub 集成。
    • 可以并且鼓励创建多个小型测试文件。
  6. 接口文件(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()(使用 ToJson trait,生成更易读的输出)。
    • 如果函数的整个返回值不大,建议检查整个返回值,这样测试会更简单。根据需要为 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:

  1. 创建目录:./fib/

  2. 添加 ./fib/moon.pkg

  3. 添加包含代码的 .mbt 文件

  4. 在依赖该包的包中导入:

      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 导入块中单独导入。

  1. 使用 moon add moonbitlang/async 添加依赖。
  2. 在可执行文件的 moon.pkg 中设置 is-main,并导入所需内容:
    import {
      "moonbitlang/async",
      "moonbitlang/async/stdio",
    }
    options(
      "is-main": true,
    )
    
  3. 定义 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:“this fn is asynchronous but not annotated with async”。请勿使用。
  • ❌ 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

评论

支持 Markdown