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. 05Moon 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. リファクタリングのテーマを 1 つ選びます(API の最小化、パッケージ分割、パターンマッチング、ループのスタイル)。
  4. 安全な範囲で最小限の変更を適用します。
  5. 同じパッチでドキュメントとテストを更新します。
  6. moon check を実行し、その後 moon test を実行します。
  7. カバレッジを使って不足している分岐を特定します。

高レベルの構造が整うまでは、局所的なクリーンアップ(名前変更、パターンマッチング)は避けてください。

パッケージアーキテクチャの改善

  • パッケージの責務を絞ります。1 万行未満を目指します。
  • ファイルを扱いやすい規模に保ちます。2,000 行未満を目指します。
  • 関数の責務を絞ります。200 行未満を目指します。
ファイルの分割

MoonBit ではファイルを整理単位として扱います。各ファイルが 1 つの概念に集中していれば、パッケージ内で自由にコードを移動できます。

パッケージの分割

パッケージ 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)
明示的な修飾を優先する
  • 明確さが重要な場合は、using ではなく @pkg.fn を使用します。
  • 大規模なリファクタリング中は、呼び出し箇所を明示的に保ちます。

例:

let n = @parser.parse_number(token)
型が既知の場合は Enum コンストラクターを簡潔にする

コンテキストから期待される型が分かる場合、Enum コンストラクターの完全なパッケージパスを省略できます。

  • パターンマッチング:マッチ対象の値に型注釈を付けると、コンストラクターのパスを省略できます。
  • ネストされたコンストラクター:完全なパスが必要なのは最外側だけです。
  • 戻り値:本体内のコンストラクターには戻り値の型からコンテキストが与えられます。
  • コレクション:コレクションに型注釈を付けると、要素にも型が引き継がれます。

例:

// 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 のコード単位が得られます。Unicode を考慮して反復するには for ch in s を使います。
小さな関数よりパターンマッチングを優先します

たとえば、

 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)と char の範囲を照合する場合に便利です。

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 を追加してください(バグの可能性があります)。

例:

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_a から package_b を切り出す場合。

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 を実行します。

コメント