mcpField
M
moonbit-refactoring

moonbit-refactoring

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

关于 moonbit-refactoring

为 MoonBit 项目提供重构指导的技能,面向维护代码库的开发者。它从架构审视开始,强调缩小公共 API、保持行为并以测试验证变更。

主要功能

  1. 01审视包结构与 API 边界
  2. 02逐步拆分包并更新调用点
  3. 03优先模式匹配与视图类型
  4. 04为重构同步补充文档和测试
  5. 05使用 Moon IDE 与覆盖率工具

适用场景

  1. 01精简不必要的公共 API
  2. 02拆分大型包或文件
  3. 03将变更控制在安全的小范围
  4. 04通过覆盖率查找待补测试分支

安装

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

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

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

附带文件LICENSE

详细介绍

MoonBit 重构技能

意图

  • 除非明确要求更改,否则保留行为和公开契约。
  • 将公共 API 精简到调用方所需的范围。
  • 优先使用声明式风格和模式匹配,而不是无关的可变操作。
  • 使用视图类型(ArrayView/StringView/BytesView)以避免复制。
  • 重构时同步添加测试和文档。

工作流

先从整体入手,再在局部细化:

  1. 先看架构:检查包结构、依赖项和 API 边界。
  2. 盘点公共 API 和调用点(moon ide doc、moon ide find-references)。
  3. 选定一个重构主题(精简 API、拆分包、模式匹配、循环风格)。
  4. 实施最小且安全的改动。
  5. 在同一个补丁中更新文档和测试。
  6. 运行 moon check,然后运行 moon test。
  7. 使用覆盖率定位缺失的分支。

在高层结构稳固之前,避免进行局部清理(重命名、模式匹配)。

改进包架构

  • 保持包的职责专一:每个包尽量少于 10k 行。
  • 保持文件易于管理:每个文件尽量少于 2k 行。
  • 保持函数职责专一:每个函数尽量少于 200 行。
拆分文件

在 MoonBit 中,将文件视为组织单元;只要每个文件都专注于一个概念,就可以在包内自由移动代码。

拆分包

将包 A 拆分为 A 和 B 时:

  1. 创建新包并临时重新导出:

    // In package B
    using @A { ... }  // re-export A's APIs
    

    继续之前,确保 moon check 通过。

  2. 查找并更新所有调用点:

    moon ide find-references <symbol>
    

    将裸写的 f 替换为 @B.f。

  3. 更新完所有调用点后,移除 use 语句。

  4. 检查并移除两个包中因此变得未使用的 pub API。

指南
  • 优先采用无环依赖:低层包不应导入高层包。
  • 只公开下游包实际需要的内容。
  • 可考虑使用 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 }

步骤:

  1. 使用 moon ide find-references <symbol> 查找 a 和 B 的所有调用位置。
  2. 将它们替换为 @package_a.a 和 @package_a.B。
  3. 移除 using 语句并运行 moon check。

评论

支持 Markdown