MoonBit Refactoring Skill
Intent
- Preserve behavior and public contracts unless explicitly changed.
- Minimize the public API to what callers require.
- Prefer declarative style and pattern matching over incidental mutation.
- Use view types (ArrayView/StringView/BytesView) to avoid copies.
- Add tests and docs alongside refactors.
Workflow
Start broad, then refine locally:
- Architecture first: Review package structure, dependencies, and API boundaries.
- Inventory public APIs and call sites (
moon doc,moon ide find-references). - Pick one refactor theme (API minimization, package splits, pattern matching, loop style).
- Apply the smallest safe change.
- Update docs/tests in the same patch.
- Run
moon check, thenmoon test. - Use coverage to target missing branches.
Avoid local cleanups (renaming, pattern matching) until the high-level structure is sound.
Improve Package Architecture
- Keep packages focused: aim for <10k lines per package.
- Keep files manageable: aim for <2k lines per file.
- Keep functions focused: aim for <200 lines per function.
Splitting Files
Treat files in MoonBit as organizational units; move code freely within a package as long as each file stays focused on one concept.
Splitting Packages
When spinning off package A into A and B:
-
Create the new package and re-export temporarily:
mbt1// In package B 2using @A { ... } // re-export A's APIsEnsure
moon checkpasses before proceeding. -
Find and update all call sites:
bash1moon ide find-references <symbol>Replace bare
fwith@B.f. -
Remove the
usestatement once all call sites are updated. -
Audit and remove newly-unused
pubAPIs from both packages.
Guidelines
- Prefer acyclic dependencies: lower-level packages should not import higher-level ones.
- Only expose what downstream packages actually need.
- Consider an
internal/package for helpers that shouldn't leak.
Minimize Public API and Modularize
- Remove
pubfrom helpers; keep only required exports. - Move helpers into
internal/packages to block external imports. - Split large files by feature; files do not define modules in MoonBit.
Local refactoring
Convert Free Functions to Methods + Chaining
- Move behavior onto the owning type for discoverability.
- Use
..for fluent, mutating chains when it reads clearly.
Example:
mbt1// Before 2fn reader_next(r : Reader) -> Char? { ... } 3let ch = reader_next(r) 4 5// After 6fn Reader::next(self : Reader) -> Char? { ... } 7let ch = r.next()
Example (chaining):
mbt1buf..write_string("#\\")..write_char(ch)
Prefer Explicit Qualification
- Use
@pkg.fninstead ofusingwhen clarity matters. - Keep call sites explicit during wide refactors.
Example:
mbt1let n = @parser.parse_number(token)
Simplify Enum Constructors When Type Is Known
When the expected type is known from context, you can omit the full package path for enum constructors:
- Pattern matching: Annotate the matched value; constructors need no path.
- Nested constructors: Only the outermost needs the full path.
- Return values: The return type provides context for constructors in the body.
- Collections: Type-annotate the collection; elements inherit the type.
Examples:
mbt1// Pattern matching - annotate the value being matched 2let tree : @pkga.Tree = ... 3match tree { 4 Leaf(x) => x 5 Node(left~, x, right~) => left.sum() + x + right.sum() 6} 7 8// Nested constructors - only outer needs full path 9let x = @pkga.Tree::Node(left=Leaf(1), x=2, right=Leaf(3)) 10 11// Return type provides context 12fn make_tree() -> @pkga.Tree { 13 Node(left=Leaf(1), x=2, right=Leaf(3)) 14} 15 16// Collections - type annotation on the array 17let trees : Array[@pkga.Tree] = [Leaf(1), Node(left=Leaf(2), x=3, right=Leaf(4))]
Pattern Matching and Views
- Pattern match arrays directly; the compiler inserts ArrayView implicitly.
- Use
..in the middle to match prefix and suffix at once. - Pattern match strings directly; avoid converting to
Array[Char]. String/StringViewindexing yieldsUInt16code units. Usefor ch in sfor Unicode-aware iteration.
we prefer pattern matching over small functions
For example,
mbt1 match gen_results.get(0) { 2 Some(value) => Iter::singleton(value) 3 None => Iter::empty() 4 }
We can pattern match directly, it is more efficient and as readable:
mbt1 match gen_results { 2 [value, ..] => Iter::singleton(value) 3 [] => Iter::empty() 4 }
MoonBit pattern matching is pretty expressive, here are some more examples:
mbt1match items { 2 [] => () 3 [head, ..tail] => handle(head, tail) 4 [..prefix, mid, ..suffix] => handle_mid(prefix, mid, suffix) 5}
mbt1match s { 2 "" => () 3 [.."let", ..rest] => handle_let(rest) 4 _ => () 5}
Char literal matching
Use char literal overloading for Char, UInt16, and Int; the examples below rely on it. This is handy when matching String indexing results (UInt16) against a char range.
mbt1test { 2 let a_int : Int = 'b' 3 if (a_int is 'a'..<'z') { () } else { () } 4 let a_u16 : UInt16 = 'b' 5 if (a_u16 is 'a'..<'z') { () } else { () } 6 let a_char : Char = 'b' 7 if (a_char is 'a'..<'z') { () } else { () } 8}
Use Nested Patterns and is
- Use
ispatterns insideif/guardto keep branches concise.
Example:
mbt1match token { 2 Some(Ident([.."@", ..rest])) if process(rest) is Some(x) => handle_at(rest) 3 Some(Ident(name)) => handle_ident(name) 4 None => () 5}
Prefer Range Loops for Simple Indexing
- Use
for i in start..<end { ... },for i in start..<=end { ... },for i in large>..small, orfor i in large>=..smallfor simple index loops. - Keep functional-state
forloops for algorithms that update state.
Example:
mbt1// Before 2for i = 0; i < len; { 3 items.push(fill) 4 continue i + 1 5} 6 7// After 8for i in 0..<len { 9 items.push(fill) 10}
Loop Specs (Dafny-Style Comments)
- Add specs for functional-state loops.
- Skip invariants for simple
for x in xsloops. - Add TODO when a decreases clause is unclear (possible bug).
Example:
mbt1for i = 0, acc = 0; i < xs.length(); { 2 acc = acc + xs[i] 3 i = i + 1 4} else { acc } 5where { 6 invariant: 0 <= i <= xs.length(), 7 reasoning: ( 8 #| ... rigorous explanation ... 9 #| ... 10 ) 11}
Tests and Docs
- Prefer black-box tests in
*_test.mbtor*.mbt.md. - Add docstring tests with
mbt checkfor public APIs.
Example:
mbt1///| 2/// Return the last element of a non-empty array. 3/// 4/// # Example 5/// ```mbt check 6/// test { 7/// inspect(last([1, 2, 3]), content="3") 8/// } 9/// ``` 10pub fn last(xs : Array[Int]) -> Int { ... }
Coverage-Driven Refactors
- Use coverage to target missing branches through public APIs.
- Prefer small, focused tests over white-box checks.
Commands:
bash1moon coverage analyze -- -f summary 2moon coverage analyze -- -f caret -F path/to/file.mbt
Moon IDE Commands
bash1moon doc "<query>" 2moon ide outline <dir|file> 3moon ide find-references <symbol> 4moon ide peek-def <symbol> 5moon ide rename <symbol> -new-name <new_name> 6moon check 7moon test 8moon info
Use these commands for reliable refactoring.
Example: spinning off package_b from package_a.
Temporary import in package_b:
mbt1using @package_a { a, type B }
Steps:
- Use
moon ide find-references <symbol>to find all call sites ofaandB. - Replace them with
@package_a.aand@package_a.B. - Remove the
usingstatement and runmoon check.