ocaml2moonbit-migration について
ocaml2moonbit-migration は、OCaml の動作、データ不変条件、公開契約を MoonBit に移植するためのガイドスキルです。文字列の意味を分類し、テストと MoonBit ツールで段階ごとに検証する方針を示します。
主な機能
- 01モジュール境界と依存関係の棚卸し
- 02バイト列とテキスト文字列の分類
- 03OCaml 型から MoonBit 型への対応付け
- 04不確かな言語仕様のプローブ
- 05MoonBit のチェックとテストによる検証
活用シーン
- 01公開動作を保ちながらモジュールを移植
- 02バイナリ解析やシリアライズコードの移行
- 03文字列、整数、バイトビューの差異の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ moonbitlang/ skills --skill ocaml2moonbit-migrationインストール後、Claude Code で/ocaml2moonbit-migrationを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yaml
詳細
OCaml から MoonBit への移行
まず動作、データ不変条件、公開コントラクトを移植します。構文の翻訳は、ソースの意味を分類してから行います。移植で最もよくあるバグは、OCaml の string(バイト列)を MoonBit の String(UTF-16 テキスト)へ暗黙に型変換してしまうことです。型を選ぶ前に、各フィールドの意味を分類してください。
判断に迷ったら、moon run -c '...' で動作を確認します。このガイドの確認用コードは moon 0.1.20260512 相当のツールチェーンで検証済みです。ローカルのツールチェーンがそれより新しく、動作が重要な場合は、該当する確認用コードを再実行してください。
移行ワークフロー
- OCaml モジュールの境界を洗い出します。公開型、関数、例外、オプション引数、可変状態、遅延状態、C/Unix/ファイルシステムへの依存関係、既存のテストやゴールデンフィクスチャが対象です。
- MoonBit の型を選ぶ前に、すべての OCaml
stringの意味を分類します。同じ OCaml レコード内でも、フィールドごとに行ってください。 - コーディングを始める前に、MoonBit のパッケージ境界とインポートを決めます。インポートは
moon.pkgに追加します。MoonBit のソースファイルでは、OCaml のopenのような構文は使いません。 - テストを使いながら、動作の単位ごとに移植します。まず薄い公開 API の骨組みを作り、その後ろにパーサーやシリアライザー、アルゴリズムの内部実装を追加します。
- 言語やライブラリの動作に不明点があれば、
moon run -cで確認し、必要に応じて OCaml のトップレベルでも簡単な確認用コードを実行します。確認用コードは最小限にしてください。 - 各単位の作業を終えるときに、
moon check、moon test、moon info、moon fmtを実行します。より厳密に確認するには、moon check --warn-list +...で警告を追加できます。
型マッピング早見表
| OCaml での用途 | MoonBit でのデフォルト |
|---|---|
| バイナリペイロード、ファイル内容、圧縮/暗号化/チェックサム付きデータ、パーサー入力 | Bytes または BytesView |
| 人が読むテキスト、診断メッセージ、真に Unicode であるラベル | String |
| 0~255 の範囲が決まっている単一バイト | Byte |
| インデックス、カウント、小さな識別子、意図的な符号付き 32 ビット折り返し演算 | Int |
| ファイルオフセット、シリアライズされた位置、大きなオブジェクト番号 | Int64 または UInt64 |
長さが固定された OCaml の array | FixedArray[T] |
| 可変長のビルダー | Array[T] |
| 読み取り専用のシーケンス引数 | ArrayView[T] または BytesView |
| コンパイル時のルックアップテーブル(リテラルまたは内包表記) | ReadOnlyArray[T] |
| 決定論的な反復処理が可能なキー付きルックアップ | Map[K, V] |
OCaml の Buffer.t によるバイトビルダー | @buffer.Buffer |
OCaml の ref | Ref[T] |
| OCaml のバリアント | enum。内部状態には priv enum がよく使われます |
| OCaml のレコード | struct。不変な更新には { ..old, field: value } を使います |
| OCaml の例外フロー | suberror とチェック付きの raise |
| 遅延状態 | Lazy[T] |
ラップアラウンドが必要な OCaml の int32 | Int |
広い範囲の演算が必要な OCaml の int32 | Int64 または UInt64 |
OCaml の float | Double |
現在のツールチェーンには Int32 型がありません。
バイトと文字列
OCaml の String.length はバイト数を数えますが、MoonBit の String::length() は UTF-16 コード単位を数えます。同じソース文字でも、2 つの言語では位置が異なり、その違いは気づかないまま生じます。
ocaml -noprompt -noinit <<'EOF'
let s = "𝄞";;
Printf.printf "%d\n" (String.length s);;
Printf.printf "%d\n" (Char.code s.[0]);;
EOF
# 4
# 240
moon run -c 'fn main { let s = "𝄞"; println(s.length()); println(s.char_length()) }'
# 2
# 1
String::length() が数えるのは UTF-16 コード単位であり、String::char_length() が数えるのは Unicode スカラー値です。どちらもバイト数ではありません。バイト指向の形式には Bytes/BytesView を使ってください。Bytes と String の変換は、エンコーディングの前提を明示する名前付きのエンコーディングヘルパー(@ascii.encode、@utf8.encode など)を通してのみ行ってください。
moon run -c 'fn main { let raw : Array[Byte] = [65, 0, 255]; let b = Bytes::from_array(raw); println(b.length()); println(b[2].to_int()) }'
# 3
# 255
moon run -c 'fn main { let source : Array[Byte] = [1, 2]; let bytes = Bytes::from_array(source); source[0] = 9; println(bytes[0].to_int()); println(source[0].to_int()) }'
# 1
# 9
Bytes::from_array は、変更不可の所有権を持つ Bytes を生成します。その後で元の配列を変更しても、凍結された値には影響しません。OCaml の Bytes のミューテーターを移植する場合は、BytesView -> Bytes の変換として実装するか、最終的に凍結するまでは状態を Array[Byte]/FixedArray[Byte] に保持してください。
moon run -c 'fn main { let empty = Bytes::new(0); let zeros = Bytes::new(2); println(empty.length()); println(zeros.length()); println(zeros[0].to_int()) }'
# 0
# 2
# 0
Bytes::new(length) は常にゼロで埋められ、明示的な長さが必要です。OCaml の Bytes.empty または Bytes.make len '\000' が使われていた箇所で使用してください。
Bytes は変更不可であるため、Bytes::copy は非推奨です。移植時に新しい物理コピーを作成する必要がある場合(たとえば、バッファーが共有されないという OCaml の保証を維持する場合)は、Bytes::makei(len, i => src[i]) を使ってください。
moon run -c $'let cached_filter = b"/Filter"\nfn main { let b = b"PDF"; println(b.length()); println(b[0].to_int()); println(cached_filter.length()); println(cached_filter[0].to_int()) }'
# 3
# 80
# 7
# 47
b"..." は、型が Bytes のコンパイル時バイト文字列リテラルです。ASCII の形式構文(b"/Filter"、b"PDF" など)にはこれを使ってください。@ascii のインポートも、実行時の呼び出しも、トップレベルでの型注釈も必要ありません。@ascii.encode(text) は、ソースが動的な String の場合の String → Bytes 変換にのみ使用してください。バイナリファイル形式を String の連結で構築しないでください。
moon run -c $'fn main { let buf = @buffer.new(); buf.write_bytes(b"PDF"); buf.write_byte(10); let bytes = buf.contents(); println(bytes.length()); println(bytes[0].to_int()); println(bytes[3].to_int()) }'
# 4
# 80
# 10
MoonBit で標準的なバイトビルダーは @buffer.Buffer(moonbitlang/core/buffer から)です。@buffer.new()(必要に応じて size_hint=N)で構築し、静的な ASCII は buf.write_bytes(b"...")、単一バイトは buf.write_byte(n)、動的なテキストは buf.write_bytes(@ascii.encode(text))、バイナリペイロードは buf.write_bytes(view) で追加し、最後に buf.contents() で凍結します。処理中のバイトをランダムアクセスで変更する必要がある場合は Array[Byte] と Bytes::from_array を使い、それ以外では Buffer を使ってください。Buffer は OCaml の Buffer.t に相当します。
moon run -c 'fn main { let s = "/UniJIS-UCS2-H"; println(s.has_prefix("/Uni")); println(s.contains("-UCS2-")); println(s.has_suffix("-H")); println(s[1:]); println(s[1:].to_owned()) }'
# true
# true
# true
# UniJIS-UCS2-H
# UniJIS-UCS2-H
判定処理には String::has_prefix、has_suffix、contains が使えます。s[start:end] でスライスできます。これは BytesView と同様、借用された StringView を返すため、低コストで検査やパターンマッチングに適しています。呼び出し先が所有権を持つ String を必要とする場合(たとえば、保存する場合や String 型のパラメーターに渡す場合)は、s[start:end].to_owned() を使ってください。オフセットはバイトではなく UTF-16 コード単位です。これらは、ASCII であることが検証済みのトークン、または実際にテキストを解析する場合にのみ使用してください。
バイトビュー
moon run -c 'fn has_ab(view : BytesView) -> Bool { match view { [65, 66, ..] => true; _ => false } }
fn main { let bytes : Bytes = [65, 66, 67]; let view = bytes[:2]; println(view.length()); println(view[0].to_int()); println(has_ab(bytes[:])); println(has_ab(view)) }'
# 2
# 65
# true
# true
BytesView は、ArrayView に対応するバイト列です。ビューは低コストなスライスで、読み取り専用のバイト操作を提供し、残余パターンを使ったパターンマッチングにも対応します。読み取り専用のバイト API には BytesView を優先して使い、.to_owned() は明示的な所有権の境界でのみ呼び出してください。BytesView::to_owned() はビュー全体の場合は元のバイト列を返しますが、部分スライスの場合はメモリーを割り当ててコピーします。
Bytes は BytesView 型のパラメーターに代入できるため、所有または借用された入力のどちらも受け取れる API を 1 つ用意できます。返された BytesView は関数の境界を越えても元になる Bytes を生存させます。これは、コピーを強制せずにデコード/復号結果を公開する場合に便利です。
逆方向の変換は自動ではありません。ネイティブ extern 宣言を含め、所有権を持つ Bytes が必要な場所では BytesView は型チェックを通りません。ここを所有権の境界とみなし、意図的に .to_owned() を呼び出してください。比較ではビューが左辺なら等価性が機能します。Bytes が左辺にある場合は、比較の前に bytes[:] でスライスしてください。
moon run -c 'fn main { let data : Bytes = [60, 65, 62, 0, 12, 60, 66, 62]; println(data.length()); println(data[3].to_int()); println(data[4].to_int()); let view = data[5:]; match view { [60, 66, 62] => println("match"); _ => println("miss") } }'
# 8
# 0
# 12
# match
NUL(0)とフォームフィード(12)は、Bytes/BytesView では通常のバイトです。OCaml のバイト判定処理はそのまま移植してください。形式の空白判定を、文字列向けのスペース/タブの判定に狭めないでください。
整数
moon run -c 'fn main { let (b, u, u16, i64, u64) : (Byte, UInt, UInt16, Int64, UInt64) = (255, 7, 65535, 42, 42); println(b.to_int()); println(u.to_string()); println(u16.to_int()); println(i64.to_string()); println(u64.to_string()) }'
# 255
# 7
# 65535
# 42
# 42
スカラー型:Byte、Int16、UInt16、Int、UInt、Int64、UInt64、Float、Double。Int32はありません。意図的に符号付き32ビットのラップアラウンドを行う場合はIntを使用し、OCamlのint32値を切り捨てずに表現する必要がある場合はInt64/UInt64を使用してください。整数リテラルのメソッドを呼び出すときは、括弧で囲みます。(65).to_byte()のように記述し、65.to_byte()とはしないでください。65.は浮動小数点数の始まりとして解析されます。
moon run -c 'fn main { let max = 2147483647; println(max + 1); println(1 << 31); println(0xF0 & 0x0F); println(0x80 >> 7); println(0xAA ^ 0xFF) }'
# -2147483648
# -2147483648
# 0
# 1
# 85
Intの算術演算は、符号付きとして解釈される2^32を法とするラップアラウンドです。記号演算子はOCamlから直接対応付けられます。&(land)、|(lor)、^(lxor)、<<(lsl)、>>(右シフト。符号付きの場合は算術シフト)。
moon run -c 'fn main { println((-8) >> 1); println(1 << 32); println(1 >> 32); let logical = ((-8).reinterpret_as_uint() >> 1).reinterpret_as_int(); println(logical) }'
# -4
# 1
# 1
# 2147483644
Intの右シフトは算術シフトで、シフト数は5ビットにマスクされるため、1 << 32 == 1となります。OCamlのInt32.shift_right_logicalを使う場合は、符号付きから符号なしに再解釈してシフトし、その後、符号なしから符号付きに再解釈してください。数値変換(.to_uint())は、ビットの再解釈(.reinterpret_as_uint())とは異なります。
moon run -c $'fn main { try @string.parse_int("2147483648") catch { _ => println(true) } noraise { _ => println(false) }; let mut value = 0; for digit in [50,49,52,55,52,56,51,54,52,56] { value = value * 10 + digit - 48 }; println(value); let mut wide = 0L; for digit in [50,49,52,55,52,56,51,54,52,56] { wide = wide * 10L + (digit - 48).to_int64() }; println(wide.to_string()) }'
# true
# -2147483648
# 2147483648
@string.parse_intは範囲外の10進数を拒否しますが、Intで手書きの桁の累積を行うと、警告なくラップアラウンドします。オフセット、長さ、オブジェクト番号、シリアライズされたカウンターはInt64/UInt64で累積し、Intに狭める前に範囲をチェックしてください。
moon run -c 'fn main { println(0x8EA2A1A1 < 0); println(0x8EA2A1A1); println(0x7FFFFFFF < 0x8EA2A1A1); println((-1) % 256) }'
# true
# -1901944415
# false
# -1
0x7FFFFFFFを超えるIntリテラルは負の値になります。符号なしバイト順でソートされたテーブルは、符号付きIntで比較すると同じ順序になりません。UInt(reinterpret_as_uint)を介して比較するか、Int64/UInt64として保存してください。%は左オペランドの符号を保持します。(a - b) mod 256形式の式は、Byteに変換する前に0..255の範囲に正規化してください。大きな基数を乗算するバイトコーデックの算術演算では、ByteをUInt64に昇格させてください。大きな値に対するUInt64::to_int()は、符号付き32ビットに切り捨てられます。
moon run -c 'fn rotr64(value : UInt64, bits : Int) -> UInt64 { (value >> bits) | (value << (64 - bits)) }
fn main { let value : UInt64 = 0x0123456789abcdef; let full : UInt64 = UInt64::lnot(0); println(rotr64(value, 8).to_string()); println((full + (1 : UInt64)).to_string()); println(((0x80 : UInt64) >> 7).to_string()) }'
# 17222085231038278605
# 0
# 1
UInt64の算術演算は、2^64を法とするラップアラウンドです。符号なし右シフトは論理シフトです。リテラルが符号付きIntの範囲を超える場合は、UInt64のコンテキスト(x : UInt64)を明示してください。
moon run -c 'fn main { println(Int::lnot(0)); println((0x0F).lnot()); println((!true).to_string()) }'
# -1
# -16
# false
ビット単位の補数:Int::lnot(value)またはvalue.lnot()。ブール値の否定:!expr。OCamlのnot xをそのまま移植しないでください。
浮動小数点数
ocaml -noprompt -noinit <<'EOF'
print_endline (string_of_float 2.);;
print_endline (string_of_float infinity);;
EOF
# 2.
# inf
moon run -c 'fn main raise { println((2.0).to_string()); println((1.0 / 0.0).to_string()); let tiny : Double = @string.from_str("1e-10"); println(tiny.to_string()) }'
# 2
# Infinity
# 1e-10
Double::to_string()は、OCamlのstring_of_floatやPrintf "%.12g"の置き換えとしてそのまま使えるものではありません。整数値の浮動小数点数は2.ではなく2と表示され、無限大はinf/-infではなくInfinity/-Infinityと表示されます。また、小さな値には科学表記が使われます。ダイジェスト、ファイル形式、スナップショットが正確な表記に依存する場合は、OCamlのフォーマッターを境界関数として明示的に移植してください。オーバーフローするとInfinityになり、無限大で除算すると0になります。防御的なfloat分岐を移植するときは、これらの境界ケースをプローブで確認してください。
moon run -c 'fn main { println(@math.pow(0.5, 2.0)); println((4.0).sqrt()); println((-3.0).abs()); println((5.5).mod(2.0)) }'
# 0.25
# 2
# 3
# 1.5
**演算子はありません。@math.pow(base, exponent)を使用してください。よく使うヘルパーはDoubleのメソッドです。value.sqrt()、value.abs()、value.mod(other)。自由関数は@mathにあります。sin、cos、atan2、ln、log10、floor、ceil、round、trunc、exp。@math.sqrt/@math.absは有効ではないので注意してください。これらはメソッドとしてのみ使用できます。パスを決めつける前に、moon ide doc @mathでプローブしてください。
データ構造
OCaml のバリアントは MoonBit の enum にマッピングします。テストで検査する型には Debug と Eq を導出します。
moon run -c 'enum E { A(Int) } derive(Debug)
fn E::value(self : E) -> Int { match self { A(n) => n } }
fn main { let value = A(7); println(value.value()) }'
# 7
moon run -c 'enum E { A(Int); B(Int) } derive(Debug)
fn main { let xs : Array[E] = [E::A(1), E::B(2)]; println(xs.length()) }'
# 2
コードがパッケージの境界を越える場合や、異種に見える enum 配列を構築する場合は、明示的な Type::Constructor 形式、または具体的な要素型の注釈を優先します。これにより、moon check --warn-list +73 を有効に活用できます。注釈を削除するのは、コンパイラーが本当に不要だと報告した場合だけにしてください。
moon run -c 'priv enum Section { NoneSection; ActiveSection }
fn choose(flag : Bool) -> Section { if flag { ActiveSection } else { NoneSection } }
fn main { match choose(true) { ActiveSection => println("active"); NoneSection => println("none") } }'
# active
moon run -c 'priv struct S { value : Int }
fn make() -> S { { value: 1 } }
fn main { let s = make(); println(s.value) }'
# 1
パッケージインターフェースに公開すべきでない内部ヘルパーには、priv enum/priv struct を使用します。実装をデバッグする経路で実際に使われない限り、プライベートなヘルパーにトレイトを導出するのは避けてください。
moon run -c 'struct S { a : Int; b : Int? } derive(Debug, Eq)
fn main { let s = { a: 1, b: None }; let t = { ..s, b: Some(2) }; match t.b { Some(value) => println(value); None => println(0) } }'
# 2
レコードの更新には { ..old, field: value } を使います。これは OCaml の { old with field = value } に直接対応します。
moon run -c 'struct S { values : Int } derive(Debug)
fn make(values : Int) -> S? { Some({ values, }) }
fn main { println(make(7).unwrap().values) }'
# 7
別の式の中でフィールド省略記法を使う単一フィールドの構造体リテラルには、曖昧なブロックに関する警告を抑制するため、末尾にカンマ({ values, })を付けます。
OCaml の関数が広範なバリアントを返し、後続のコードが非形式的な不変条件に依存する場合は、チェック後の状態を表す限定的なプライベート MoonBit enum を導入します。これにより、広範な Object のような値を移植の残りの部分まで持ち回るのではなく、不可能なフォールバック分岐を型の境界で明示できます。
ルックアップテーブル
moon run -c 'fn range_lookup(ranges : ArrayView[(Int, Int, Int)], key : Int) -> Int? { for range in ranges { if key >= range.0 && key <= range.1 { return Some(range.2 + key - range.0) } }; None }
fn main { let ranges = [(0x21, 0x23, 100), (0x30, 0x30, 200)]; println(range_lookup(ranges, 0x22).unwrap()); println(range_lookup(ranges, 0x24) is None) }'
# 101
# true
大規模な OCaml のマッピングテーブルには、すべてのエントリーを機械的に展開するのではなく、ArrayView が受け付けるコンパクトな範囲テーブル((first, last, base) と小規模な例外/シーケンステーブルの組み合わせ)を優先します。生成される MoonBit ソースが小さくなり、ネイティブターゲットの検証も速くなります。ルックアップの動作は決定的なままです。
moon run -c $'let table : ReadOnlyArray[Int?] = [for i in 0..<8 => if i == 5 { Some(10) } else { None }]\nfn lookup(i : Int) -> Int? { if i >= 0 && i < table.length() { table[i] } else { None } }\nfn main { println(lookup(5).unwrap()); println(lookup(0) == None); println(lookup(99) == None); println(table.length()) }'
# 10
# true
# true
# 8
密なルックアップ領域では、トップレベルのプライベートな let table : ReadOnlyArray[T?] = [...] を定義します。ReadOnlyArray[T] は Array[T] に対応する読み取り専用型です。配列リテラルはそのまま型変換され、リスト内包表記も ReadOnlyArray[T] を生成します。そのため、makei を使わずにコンパイル時テーブルを生成する慣用的な方法は [for i in 0..<N => entry(i)] です。境界チェックはルックアップの境界で行います。
ミューテーション、Ref、配列
moon run -c $'fn first_sorted(xs : ArrayView[Int]) -> Int { let ys = xs.to_owned(); ys.sort(); ys[0] }\nfn main { let r : Ref[Int] = Ref::{ val: 0 }; r.val += 1; println(r.val); let fixed : FixedArray[Int] = [3, 1, 2]; let grow = [4, 2, 3]; println(first_sorted(fixed)); println(first_sorted(grow)); println(fixed[0]); println(grow[0]) }'
# 1
# 1
# 2
# 3
# 4
プリミティブ値のミュータビリティには Ref[T] を、大きな状態には構造体内の mut フィールドを使います。Array は拡張可能なミュータブルベクターで、FixedArray は OCaml の array により直接対応します。ArrayView[T] は読み取り専用のシーケンスパラメーター型です。呼び出し側はコピーせずに Array、FixedArray、または別のビューを渡せます。.to_owned() は、明示的な所有権の境界(例:sort()/sort_by() の前)でミュータブルな Array を実体化します。ホットパスでは使用しないでください。遅延状態/保留状態は、ネストしたミュータブルコンテナーではなく、明示的な enum バリアントで表現します。
moon run -c 'fn main { let xs = [(2, "b"), (1, "a")]; xs.sort_by(fn(a, b) { a.0.compare(b.0) }); println(xs[0].0); let ys = [1, 2, 3]; let zs = ys.rev(); println(zs[0]); println(ys[0]) }'
# 1
# 3
# 1
Array::sort_by は Int を返す比較関数を使って、その場でソートします。Array::rev() は反転したコピーを返し、元の配列は変更しません。これは、逆順に cons でリストを構築し、条件に応じて List.rev を適用する OCaml コードを移植する際に便利です。
moon run -c $'fn main { let m : Map[Int, String] = Map([]); m[3] = "three"; m[1] = "one"; m[2] = "two"; for k, v in m { println("\\{k}=\\{v}") } }'
# 3=three
# 1=one
# 2=two
キーによるルックアップには、組み込みの Map[K, V] を優先します。これは挿入順を保持する連結ハッシュマップなので、反復は決定的で、キーを記述した順序と一致します。Map([])(または Map([], capacity=N))で構築します。Map::new() は非推奨です。@hashmap.HashMap[K, V](moonbitlang/core/hashmap 由来)は、挿入順が重要でなく、わずかに低コストな構造が必要な場合に限って使用します。重複キーが必要な場合や、シーケンスとして処理する場合に限り、ペアの順序付き配列を使います。
エラー
OCaml の例外(Not_found、End_of_file、Invalid_argument、ドメインエラー)を、チェックされない制御フローとしてそのままコピーしてはいけません。
moon run -c $'suberror E\nfn make(flag : Bool) -> ((Int) -> Int?) raise E { if flag { raise E } else { fn(x) { Some(x) } } }\nfn main raise { println(make(false)(3).unwrap()) }'
# 3
moon run -c $'fn parse(s : String) -> Int raise { @string.parse_int(s) }\nfn helper() -> Int raise { parse("7") }\ntest "raising test body can propagate" { let value = parse("7"); @test.assert_eq(value, 7) }\nfn main raise { println(helper().to_string()) }'
# 7
最初の失敗する可能性がある関数を移植したら、プロジェクトレベルの suberror を一度定義します。失敗する可能性がある関数では、raise ProjectError または単に raise を宣言します。main 自体は fn main raise { ... } と宣言できます。これは、失敗する可能性がある API を呼び出すプローブや小さなエントリーポイントを書く最も簡潔な方法で、try! の定型コードは不要です。エラーを送出する関数、エラーを送出するテストヘルパー、または test 本文の中では、失敗する可能性がある各呼び出しの周囲に明示的な try! を追加しないでください。チェック対象のエラーは、これらのコンテキストから自動的に伝播します。fail を呼び出す可能性があるテストヘルパーは、test ブロックからのみ呼び出される場合でも raise を宣言する必要があります。
エラーを送出する関数が別の関数を返す場合は、raise の前に関数型を括弧で囲みます。-> ((A) -> B) raise E とし、-> (A) -> B raise E とはしないでください(後者では raise が戻り値の関数型に結び付きます)。
moon run -c $'fn may_fail(flag : Bool) -> Unit raise { if flag { fail("boom") } }\nfn main { try may_fail(true) catch { _ => println(true) } noraise { _ => println(false) } }'
# true
moon run -c $'fn parse(s : String) -> Int raise { @string.parse_int(s) }\nfn main {\n try parse("7") catch {\n _ => println(0)\n } noraise {\n value => println(value)\n }\n}'
# 7
try expr catch { ... } noraise { value => ... } は、成功とエラーの分岐を明示する OCaml の try ... with ... に相当する構文です。エラーを伝播するだけの場合は、raise コンテキストから失敗する可能性がある関数を直接呼び出してください。
プロジェクトのエラー型を維持しながら特定のコンストラクターを捕捉し、それ以外を伝播するには、expr catch { SpecificError => fallback; error => raise error } を使用します。catch の各分岐は網羅性がチェックされるため、最後に伝播する分岐を省略しないでください。OCaml の例外テストを移植する場合、「これはエラーを送出するはず」というテストにはワイルドカードの catch 分岐を使用します。特定のバリアントがテスト対象の契約そのものである場合に限り、そのバリアントにマッチさせてください。
チェック対象のエラーを、制御フローのために汎用的な成功/失敗値へ変換しないでください。両方の分岐が必要な場合は try f() catch { ... } noraise { ... } を優先し、エラーを伝播すべき場合は raise コンテキストから直接呼び出してください。
関数とコールバック
moon run -c 'fn helper(x : Int) -> Int { x + 1 }
fn main { println(helper(1)) }'
# 2
トップレベルのヘルパー関数(main 以外のすべての関数)には、明示的な戻り値型注釈が必要です。これは moon run -c のプローブにも当てはまります。
moon run -c 'fn[A, B, C] apply_pair(f : (A, B) -> C, a : A, b : B) -> (C, A) { (f(a, b), a) }
fn main { let result = apply_pair(fn(x, y) { x + y }, 2, 3); println(result.0); println(result.1) }'
# 5
# 2
多相関数では、型パラメーターを名前の前に記述します。fn[A, B, C] name(...) の形式です。以前の fn name[A, B, C](...) という書き方は非推奨です。
moon run -c 'fn apply_twice(f : (Int) -> Int, value : Int) -> Int { f(f(value)) }
fn main { println(apply_twice(fn(x) { x + 1 }, 40)) }'
# 42
コールバックのパラメーターには (Int) -> Int のような関数型を使用し、コールバックリテラルは fn(x) { ... } と記述します。クロージャ内で可変な Ref の状態をキャプチャする方法は、OCaml で参照を使うクロージャを移植する場合と同じです。
moon run -c 'fn apply(f : (Int) -> Int raise, x : Int) -> Int raise { f(x) }
fn main raise { println(apply(fn(x) raise { if x == 0 { fail("zero") }; x + 1 }, 1)) }'
# 2
エラーを送出するコールバックでは、パラメーター型とリテラルの両方に raise を含める必要があります。(Int) -> Int raise および fn(x) raise { ... } の形式です。fn リテラルでエラー送出効果を推論する方法は非推奨です。プロジェクトのより限定的なエラー型を使う場合は、fn(x) raise ProjectError { ... } のように注釈を付けます。
moon run -c $'fn apply(f : (Int) -> Int raise Error) -> Int raise Error { f(1) }\nfn main { try apply(fn(x) raise Error { if x > 0 { fail("x") } else { x } }) catch { _ => println(true) } noraise { _ => println(false) } }'
# true
エラーを送出するコールバック型と、送出しないコールバック型は相互に置き換えられません。エラーを送出するクロージャをエラー非送出のスロットに渡すことも、その逆もできません。実装で後から失敗する可能性がある API(ファイルシステム、非同期処理、乱数など)にアクセスする OCaml のコールバックを移植する場合は、最初から型に raise を宣言してください。コールバック内にエラーを隠さないでください。
ラベル付き引数とデフォルト引数
moon run -c $'fn greet(name? : String = "pdf") -> String { name }\nfn main { println(greet()); println(greet(name="moon")) }'
# pdf
# moon
デフォルト値はラベル付きパラメーターに設定します:name? : T = default。呼び出し側では name=value を渡します。ラベルのない位置引数にはデフォルト値を設定しないでください。
moon run -c 'fn inner(flag? : Bool = false) -> Bool { flag }
fn outer(flag? : Bool = false) -> Bool { inner(flag~) }
fn main { println(outer()); println(outer(flag=true)) }'
# false
# true
label~ は、同じ名前のラベル付き引数の現在の値をラッパー層を通して転送します。ヘルパー間で OCaml の省略可能引数のセマンティクスを保つための、MoonBit における対応機能です。
moon run -c 'fn takes_view(xs : ArrayView[Int]) -> Int { xs.length() }
fn sample(xs? : ArrayView[Int] = []) -> Int { takes_view(xs) }
fn main { println(sample()); println(sample(xs=[1, 2, 3])) }'
# 0
# 3
省略可能なビュー引数には [] をデフォルト値として設定できます。省略可能な引数のみを渡す場合は、ラベルを指定する必要があります(sample(xs=[1, 2, 3]) とし、sample([1, 2, 3]) とはしません)。
moon run -c 'fn f(a : Int, flag? : Bool = true, xs? : ArrayView[Int] = []) -> Int { if flag { a + xs.length() } else { a - xs.length() } }
fn main { println(f(3, xs=[1, 2], flag=false)); println(f(3, flag=true)); println(f(3)) }'
# 1
# 3
# 3
必須の位置引数の後には、複数の省略可能引数を任意の順序でラベルを指定して渡せます。
デフォルト値ではローカル関数を呼び出せます。構造化レコードや配列のデフォルト値を用意する場合に便利です。
OCaml のパラメーターで「不在」「明示的な None」「明示的な Some(x)」が実際に区別される場合(つまり、3つの異なる状態を持つ場合)は、Option 型のラベル付き引数(x? : T? = None)を使用できます。この場合も、label~ による転送は機能します。ただし、これはまれなケースです。一般的なケースでは x? : T = default_value で十分です。同じ意味を通常の型付きデフォルト値で表せる場合は、Option 型のラベルを使わないでください。
パターンマッチング
moon run -c 'enum E { A(Int); B }
fn f(e : E) -> Int { match e { A(n) if n > 0 => n; _ => 0 } }
fn main { println(f(A(3))); println(f(A(-1))); println(f(B)) }'
# 3
# 0
# 0
パターンガードは同じアームに記述します:Pattern if condition => ...。if を独立した行に分けないでください。
マッチアームは上から下へ評価されます。具体的なタプルやバリアントのケースは、広範なワイルドカードやガード付きの包括的なアームより前に記述してください。広範なアームにガードを付けると、後続の具体的なケースが動作上到達不能になることがありますが、コンパイラーが常にそれを検出するとは限りません。
moon run -c 'fn main { let xs = [1, 2, 3]; match xs { [_, .. rest] => { let owned = [ for x in rest => x ]; println(rest.length()); println(owned.length()); println(owned[0]) }; _ => () } }'
# 2
# 2
# 2
配列の残余パターン(.. rest)は読み取り専用ビューを束縛します。列挙型のペイロードや API が所有権を持つ Array[T] を必要とする場合は、内包表記でコピーします:[ for x in rest => x ]
ループと内包表記
moon run -c 'fn main { let fixed : FixedArray[Int] = [1, 2, 3]; let doubled = [ for x in fixed => x * 2 ]; println(doubled.length()); println(doubled[2]) }'
# 3
# 6
moon run -c 'fn main { let pairs = [(1, 2), (3, 4)]; let firsts = [ for pair in pairs => pair.0 ]; println(firsts[0]); println(firsts[1]) }'
# 1
# 3
内包表記の構文:[ for x in xs => expr ]。束縛変数にはシンプルな識別子を使い、タプルの分解やフィールドへのアクセスは本体内で行ってください。
moon run -c $'fn parse(s : String) -> Int raise { @string.parse_int(s) }\nfn main { let _ = [ for s in ["1"] => parse(s) ]; println("done") }'
# Error: calling function with error is not allowed inside list comprehension.
内包表記の本体では、エラーを発生させる関数を呼び出すことはできません。エラーを発生させるマッパーを使う OCaml の List.map/Array.map は、エラー伝播が可能な関数内で明示的なループに置き換えてください。各結果を出力配列に追加し、エラーをそのまま伝播させます。
moon run -c 'fn main { let m : Map[Int, Int] = Map([]); m[1] = 10; m[2] = 20; let mut total = 0; for key in m.keys() { total += key }; println(total) }'
# 3
for x in xs は配列やビューだけでなく、あらゆるイテラブルを反復処理します。Map、@hashmap.HashMap、範囲、ビューはいずれもそのまま使用できます。ループのためだけにイテラブルを Array に変換しないでください。.to_array()/.to_owned() は、所有権を持つスナップショット、ソート、インデックスアクセス、変更が必要な場合に限って使ってください。
moon run -c 'fn first_positive(xs : Array[Int]) -> Int? { let mut i = 0; while i < xs.length() { if xs[i] > 0 { break Some(xs[i]) }; i += 1 } nobreak { None } }
fn main { println(first_positive([-2, 0, 7]).unwrap()); println(first_positive([-2, 0]) is None) }'
# 7
# true
while ループは break value で値を返せます。条件が false になったときの分岐は nobreak { ... } です。古い else の表記は非推奨です。新しい移植コードでは、古い関数型の loop ... { ... } 形式は避けてください。MoonBit はこれに警告を出します。
構文上の注意点
aliasは予約語です。警告を有効にしたチェックをクリーンに保つため、ローカル名、ループの束縛変数、ヘルパー名として使わないでください。- JavaScript バックエンドでは、ローカルのテスト結果に
undefinedのようなセンチネル名を使うのも避けてください。ターゲットをまたいだパッケージテストで、生成された JavaScript の名前衝突が明らかになることがあります。 - ソースファイルには OCaml スタイルの
openはありません。moon.pkgにパッケージインポートを追加し、インポートしたパッケージは@aliasを付けて呼び出してください。 - 追加のパッケージが必要な
moon run -cのスニペットでは、先頭に MBTX インポートブロックを使用します:import { "moonbitlang/x/crypto" }(複数のパッケージはカンマ区切りで記述します)。ソースファイル形式のimport "moonbitlang/core/encoding/ascii"はコマンドスニペットでは拒否されます。
プロセスと乱数
@env(moonbitlang/core/env 内)は、args() -> Array[String]、current_dir() -> String?、get_env_var(String) -> String?、get_env_vars() -> Map[String, String]、now() -> UInt64、set_env_var、unset_env_var を提供します。current_dir() はオプショナルです。一部のプラットフォームでは、常に現在のディレクトリを特定できるとは限りません。非推奨の .is_some() よりも x is Some(_) を優先してください。
moon run -c 'fn main { let r = @random.Rand::chacha8([email protected]("01234567890123456789012345678901")); println(r.uint(limit=256).to_string()); println(r.uint(limit=256).to_string()) }'
# 21
# 42
@random は決定論的な PRNG を提供します。Rand::new() は再現可能であり、OS のエントロピーではありません。セキュリティ上重要な OCaml コード(AES IV、ソルト、ファイルキー)を移植する際に、暗号学的な乱数ソースの代わりとして @random や @env.now() を使わないでください。明示的な乱数プロバイダーを維持するか、ターゲット固有のセキュアな API を使用してください。
ターゲットと非同期 I/O
moon run --target native -c $'#cfg(target="native")\nfn target_name() -> String { "native" }\n#cfg(not(target="native"))\nfn target_name() -> String { "other" }\nfn main { println(target_name()) }'
# native
宣言の直前の行に単独で記述する #cfg(...) により、ターゲット固有の実装を指定できます。#cfg と宣言を同じ行に記述すると、属性は使用されません。
MoonBit の非同期処理には await キーワードがありません。非同期関数から他の非同期関数を直接呼び出します。
Bytesに対する CPU 負荷の高い純粋な変換は同期処理のままにしてください。- ファイルシステムやネットワークにアクセスする場合は、エントリーポイントを非同期にしてください。
- ファイルの内容を
Bytesに読み込んでから同期処理の中核を呼び出す、非同期ラッパーを優先してください。これにより、すべての再帰ヘルパーを非同期にせずに済みます。 - 非同期のエントリーポイントとテストでは、関連する
moon.pkgにmoonbitlang/asyncが必要です。構文だけでは不十分です。 - 非同期関数はデフォルトでエラーを送出できます。エラーを伝播させるだけの目的で、非同期関数に
raiseを追加しないでください。未処理のエラーを拒否する必要がある場合に限り、noraiseを追加してください。
moon run --target native -c 'async fn main { println("async ok") }'
# Error: Cannot use `async fn main`: package moonbitlang/async is not imported.
moon run --target native -c $'import {\n "moonbitlang/async",\n "moonbitlang/async/fs" @fs,\n}\nasync fn main {\n try @fs.read_file("/tmp/definitely-missing-fixture") catch {\n _ => println(true)\n } noraise {\n _ => println(false)\n }\n}'
# true
移植後も、純粋な中核部分を非ネイティブターゲットで利用できるようにする場合は、非同期処理やファイルシステムのラッパーをネイティブ専用パッケージに配置し、中核部分には async をインポートしないでください。インポートはパッケージ単位です。supported_targets = "+native" はバックエンドの組み込みを制御します。単一のテストファイルを options(targets: { "file_test.mbt": [ "native" ] }) でターゲット制限できますが、インポートはパッケージ単位のため、非ネイティブのチェックでは未使用と報告される場合があります。
テスト
テストファイルの役割:
*_test.mbt:ブラックボックステストです。パッケージエイリアスを通して、公開 API のみを呼び出します。*_wbtest.mbt:ホワイトボックステストです。パッケージ内で実行され、非公開ヘルパーもテストできます。*.mbt.md:チェック済みコードブロックを含むドキュメントです。コンパイルとテストが必要なコードにはmbt check、例示のみのスニペットにはmbt nocheckを使用します。
アサーションのスタイル:
- 安定したスカラー値または構造的な結果には
@test.assert_eqを使用します(Debug制約があり、非推奨のShowベースの経路を避けられます)。 - 真偽値のアサーションには
@test.assert_eq(condition, true)を使用します。@test.assert_trueヘルパーはありません。 @test.assert_eqは、共通アサーションヘルパー用の名前付きmsg=引数を受け付けます。@test.assert_eq、fail、またはエラーを返す可能性のある API を呼び出す共通テストヘルパーでは、より限定的なプロジェクト固有のエラー型を使わない場合、-> Unit raise Errorを宣言してください。- パターンチェックには、
assert_true(value is Pattern(...))またはguard ... else { fail(...) }を使用します。 - スナップショットには、小さな値には
inspect(value, content="...")、Debugを実装する複雑な値にはdebug_inspect(value, content=...)を使用します。期待されるスナップショットが不明な場合は、inspect(value)と記述してmoon test --updateを実行し、差分を確認してください。 - エラーを送出する関数の成功パスでは、エラーが発生したらテストが失敗するよう、エラーを返す API をそのまま呼び出します。失敗が予想される場合は、
try f() catch { err => inspect(err) } noraise { _ => fail("expected to fail") }を使用します。 - 「ここではエラーが送出されるはず」というケースには、
catch節でワイルドカードの分岐を使用してください。特定のエラーバリアントが互換性の契約そのものに含まれる場合に限り、そのバリアントに一致させます。
moon run -c 'fn helper() -> Unit raise Error { @test.assert_eq(true, true); @test.assert_eq([1, 2], [1, 2]) }
fn main raise { helper() }'
moon run -c 'import { "moonbitlang/core/test" }
fn helper() -> Unit raise { @test.assert_eq(true, true, msg="named assertion") }
fn main raise { helper(); println("ok") }'
# ok
パーサー/シリアライザー、エンコーダー/デコーダー、またはローダー/ライターのペアでは、対象を絞ったエッジケースのテストに加え、少なくとも 1 つの公開 API を使ったラウンドトリップテストを組み合わせてください。ラウンドトリップテストでは、単独のユニットテストでは見落とされる所有権、バイト/テキスト、オブジェクト境界に関する誤りを検出できます。
OCaml の呼び出し元が依存していた、バイト/テキストのエッジケースをカバーしてください。非 ASCII、NUL、フォームフィード、上位ビットが立ったバイト、整数オーバーフロー、空の入力、境界オフセットです。
コマンド早見表
moon run -c 'fn main { ... }' # quick language/API probe
moon check --warn-list +73 # fast type check with extra warnings
moon test --outline # list discovered tests
moon test # run all tests
moon test path/to/file_test.mbt # run one test file
moon test package/dir --filter 'glob' # targeted test glob
moon test --update # refresh snapshots, then review diff
moon test --target native # required for async I/O tests
moon coverage analyze > uncovered.log # coverage report
moon info && moon fmt # final interface update and format
各移植パッチの検証ルール:
- 移植したコードを追加または更新する前、もしくは同時にテストを追加または更新します。
- 開発中は対象を絞って
moon testを実行します。 - 最後に
moon check --warn-list +73、moon test、moon info、moon fmtを実行します。
更新の方針
移行を通じて、このガイドと矛盾する、または内容を補足する再利用可能なルールが得られた場合は、次の内容をこのファイルに反映します。
- 置き換え対象となる OCaml の動作。
- 採用した MoonBit の API またはイディオム。
- 最小限の
moon run -c検証コードと、その実行結果。 - 既知の非互換性、ターゲットの制限、または保留中の動作。