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
Files in MoonBit are just organizational—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:
mbt// In package B using @A { ... } // re-export A's APIsEnsure
moon checkpasses before proceeding. - •
Find and update all call sites:
bashmoon 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:
// Before
fn reader_next(r : Reader) -> Char? { ... }
let ch = reader_next(r)
// After
fn Reader::next(self : Reader) -> Char? { ... }
let ch = r.next()
Example (chaining):
buf..write_string("#\\")..write_char(ch)
Prefer Explicit Qualification
- •Use
@pkg.fninstead ofusingwhen clarity matters. - •Keep call sites explicit during wide refactors.
Example:
let n = @parser.parse_number(token)
Simplify Constructors When Type Is Known
- •Drop
TypePath::Constrwhen the surrounding type is known.
Example:
match tree { // the type of tree is known to be Tree
Leaf(x) => x // no need for Tree::Leaf
Node(left~, x, right~) => left.sum() + x + right.sum()
}
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.
Examples:
match items {
[] => ()
[head, ..tail] => handle(head, tail)
[..prefix, mid, ..suffix] => handle_mid(prefix, mid, suffix)
}
match s {
"" => ()
[.."let", ..rest] => handle_let(rest)
_ => ()
}
Char literal matching
MoonBit allows char literal overloading for Char, UInt16, and Int, so the examples below work. This is handy when matching String indexing results (UInt16) against a char range.
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 { () }
}
Use Nested Patterns and is
- •Use
ispatterns insideif/guardto keep branches concise.
Example:
match token {
Some(Ident([.."@", ..rest])) if process(rest) is Some(x) => handle_at(rest)
Some(Ident(name)) => handle_ident(name)
None => ()
}
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:
// Before
for i = 0; i < len; {
items.push(fill)
continue i + 1
}
// After
for i in 0..<len {
items.push(fill)
}
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:
for i = 0, acc = 0; i < xs.length(); {
acc = acc + xs[i]
i = i + 1
} else { acc }
where {
invariant: 0 <= i <= xs.length(),
reasoning: (
#| ... rigorous explanation ...
#| ...
)
}
Tests and Docs
- •Prefer black-box tests in
*_test.mbtor*.mbt.md. - •Add docstring tests with
mbt checkfor public APIs.
Example:
///|
/// 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 { ... }
Coverage-Driven Refactors
- •Use coverage to target missing branches through public APIs.
- •Prefer small, focused tests over white-box checks.
Commands:
moon coverage analyze -- -f summary moon coverage analyze -- -f caret -F path/to/file.mbt
Moon IDE Commands
moon doc "<query>" moon ide outline <dir|file> moon ide find-references <symbol> moon ide peek-def <symbol> moon check moon test moon info
These commands are useful for reliable refactoring.
Example: spinning off package_b from package_a.
Temporary import in package_b:
using @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.