关于 moonbit-refactoring
为 MoonBit 项目提供重构指导的技能,面向维护代码库的开发者。它从架构审视开始,强调缩小公共 API、保持行为并以测试验证变更。
主要功能
- 01审视包结构与 API 边界
- 02逐步拆分包并更新调用点
- 03优先模式匹配与视图类型
- 04为重构同步补充文档和测试
- 05使用 Moon IDE 与覆盖率工具
适用场景
- 01精简不必要的公共 API
- 02拆分大型包或文件
- 03将变更控制在安全的小范围
- 04通过覆盖率查找待补测试分支
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ moonbitlang/ skills --skill moonbit-refactoring安装后在 Claude Code 中输入/moonbit-refactoring确认已加载。
附带文件LICENSE
详细介绍
MoonBit 重构技能
意图
- 除非明确要求更改,否则保留行为和公开契约。
- 将公共 API 精简到调用方所需的范围。
- 优先使用声明式风格和模式匹配,而不是无关的可变操作。
- 使用视图类型(ArrayView/StringView/BytesView)以避免复制。
- 重构时同步添加测试和文档。
工作流
先从整体入手,再在局部细化:
- 先看架构:检查包结构、依赖项和 API 边界。
- 盘点公共 API 和调用点(
moon ide doc、moon ide find-references)。 - 选定一个重构主题(精简 API、拆分包、模式匹配、循环风格)。
- 实施最小且安全的改动。
- 在同一个补丁中更新文档和测试。
- 运行
moon check,然后运行moon test。 - 使用覆盖率定位缺失的分支。
在高层结构稳固之前,避免进行局部清理(重命名、模式匹配)。
改进包架构
- 保持包的职责专一:每个包尽量少于 10k 行。
- 保持文件易于管理:每个文件尽量少于 2k 行。
- 保持函数职责专一:每个函数尽量少于 200 行。
拆分文件
在 MoonBit 中,将文件视为组织单元;只要每个文件都专注于一个概念,就可以在包内自由移动代码。
拆分包
将包 A 拆分为 A 和 B 时:
-
创建新包并临时重新导出:
// In package B using @A { ... } // re-export A's APIs继续之前,确保
moon check通过。 -
查找并更新所有调用点:
moon ide find-references <symbol>将裸写的
f替换为@B.f。 -
更新完所有调用点后,移除
use语句。 -
检查并移除两个包中因此变得未使用的
pubAPI。
指南
- 优先采用无环依赖:低层包不应导入高层包。
- 只公开下游包实际需要的内容。
- 可考虑使用
internal/包存放不应暴露的辅助代码。
精简公共 API 并模块化
- 移除辅助函数的
pub,仅保留必需的导出项。 - 将辅助函数移入
internal/包,以阻止外部导入。 - 按功能拆分大型文件;在 MoonBit 中,文件不定义模块。
局部重构
将自由函数转换为方法并进行链式调用
- 将行为移至所属类型,以便于发现。
- 链式调用清晰易读时,使用
..进行流畅的可变操作。
示例:
// Before
fn reader_next(r : Reader) -> Char? { ... }
let ch = reader_next(r)
// After
#as_free_fn(reader_next, deprecated="Use Reader::next instead")
fn Reader::next(self : Reader) -> Char? { ... }
let ch = r.next()
为平滑过渡,可在方法上添加 #as_free_fn(old_name, ...);它会生成一个已弃用的自由函数 old_name,并将调用转发给该方法。
然后查看警告,检查调用点并逐步更新。
链式调用示例:
buf..write_string("#\\")..write_char(ch)
优先使用显式限定
- 需要清晰表达时,使用
@pkg.fn而不是using。 - 大范围重构期间,保持调用点显式。
示例:
let n = @parser.parse_number(token)
已知类型时简化枚举构造器
如果上下文中已知预期类型,枚举构造器可以省略完整的包路径:
- 模式匹配:为匹配值添加类型注解;构造器无需路径。
- 嵌套构造器:只有最外层需要完整路径。
- 返回值:返回类型会为函数体中的构造器提供上下文。
- 集合:为集合添加类型注解;元素会继承该类型。
示例:
// Pattern matching - annotate the value being matched
let tree : @pkga.Tree = ...
match tree {
Leaf(x) => x
Node(left~, x, right~) => left.sum() + x + right.sum()
}
// Nested constructors - only outer needs full path
let x = @pkga.Tree::Node(left=Leaf(1), x=2, right=Leaf(3))
// Return type provides context
fn make_tree() -> @pkga.Tree {
Node(left=Leaf(1), x=2, right=Leaf(3))
}
// Collections - type annotation on the array
let trees : Array[@pkga.Tree] = [Leaf(1), Node(left=Leaf(2), x=3, right=Leaf(4))]
模式匹配和视图
- 直接对数组进行模式匹配;编译器会隐式插入 ArrayView。
- 使用中间的
..,同时匹配前缀和后缀。 - 直接对字符串进行模式匹配;避免转换为
Array[Char]。 String/StringView索引会返回UInt16代码单元。使用for ch in s进行 Unicode 感知的迭代。
我们更倾向于用模式匹配替代小函数
例如,
match gen_results.get(0) {
Some(value) => Iter::singleton(value)
None => Iter::empty()
}
我们可以直接进行模式匹配,这通常更清晰,也同样易读:
match gen_results {
[value, ..] => Iter::singleton(value)
[] => Iter::empty()
}
MoonBit 的模式匹配表达力很强,下面是更多示例:
match items {
[] => ()
[head, ..tail] => handle(head, tail)
[..prefix, mid, ..suffix] => handle_mid(prefix, mid, suffix)
}
match s {
"" => ()
[.."let", ..rest] => handle_let(rest)
_ => ()
}
Char 字面量匹配
为 Char、UInt16 和 Int 使用 Char 字面量重载;以下示例依赖此功能。匹配 String 索引结果(UInt16)与字符范围时,这很方便。
test {
let a_int : Int = 'b'
if (a_int is 'a'..<'z') { () } else { () }
let a_u16 : UInt16 = 'b'
if (a_u16 is 'a'..<'z') { () } else { () }
let a_char : Char = 'b'
if (a_char is 'a'..<'z') { () } else { () }
}
使用嵌套模式和 is
- 在
if/guard中使用is模式,让分支更简洁。
示例:
match token {
Some(Ident([.."@", ..rest])) if process(rest) is Some(x) => handle_at(rest)
Some(Ident(name)) => handle_ident(name)
None => ()
}
尽可能优先使用函数式循环,而非修改状态
- 使用函数式状态更新
示例:
// Before
let mut a = 1
let mut b = 2
for i = 0 {
if i >= n {
break
}
a = a + b
b = b + a
continue i + 1
}
for i = 0, a = 1, b = 2 {
if i >= n {
break a
}
continue i + 1, b, b + a
}
- 函数式循环也支持范围循环,因此可以简化:
for _ in 0..<n; a = 1, b = 2 {
continue b, a + b
} nobreak {
a
}
优先使用范围循环,而非简单索引
- 对于简单的索引循环,使用
for i in start..<end { ... }、for i in start..<=end { ... }、for i in large>..small或for i in large>=..small。 - 如果 xs 是迭代器(例如 Array),使用
for x in xs {...};如果还需要使用索引,则使用for i,x in xs {...} - 对于会更新状态的算法,保留函数式状态
for循环。
示例:
// Before
for i = 0; i < len; i = i + 1{
items.push(fill)
}
// After
for i in 0..<len {
items.push(fill)
}
循环规约(Dafny 风格注释)
- 为函数式状态循环添加规约。
- 对简单的
for x in xs循环,省略不变式。 - 当 decreases 子句不明确时,添加 TODO(可能存在 bug)。
示例:
for i = 0, acc = 0; i < xs.length(); {
acc = acc + xs[i]
i = i + 1
} nobreak { acc }
where {
invariant: 0 <= i <= xs.length(),
reasoning: (
#| ... rigorous explanation ...
#| ...
)
}
测试和文档
- 优先在
*_test.mbt或*.mbt.md中编写黑盒测试。 - 为公共 API 使用
mbt check围栏代码块添加文档字符串测试,并通过moon check && moon test验证。
示例:
///|
/// Return the last element of a non-empty array.
///
/// # Example
/// ```mbt check
/// test {
/// inspect(last([1, 2, 3]), content="3")
/// }
/// ```
pub fn last(xs : Array[Int]) -> Int { ... }
以覆盖率驱动重构
- 利用覆盖率,通过公共 API 定位缺失分支。
- 优先编写小型、聚焦的测试,而非白盒检查。
命令:
moon coverage analyze -- -f summary
moon coverage analyze -- -f caret -F path/to/file.mbt
Moon IDE 命令
moon ide doc "<query>"
moon ide outline <dir|file>
moon ide find-references <symbol>
moon ide peek-def <symbol>
moon ide rename <symbol> <new_name>
moon ide analyze [path]
moon check
moon test
moon info
使用这些命令进行可靠的重构。
示例:将 package_b 从 package_a 中拆分出来。
在 package_b 中临时导入:
using @package_a { a, type B }
步骤:
- 使用
moon ide find-references <symbol>查找a和B的所有调用位置。 - 将它们替换为
@package_a.a和@package_a.B。 - 移除
using语句并运行moon check。
评论
支持 Markdown