mcpField
O
ocaml2moonbit-migration

ocaml2moonbit-migration

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

关于 ocaml2moonbit-migration

ocaml2moonbit-migration 是将 OCaml 行为、数据不变量和公共契约迁移到 MoonBit 的指南型 Skill。它强调先区分字符串语义,并通过测试和 MoonBit 工具验证每个迁移部分。

主要功能

  1. 01盘点模块边界与依赖
  2. 02区分字节数据和文本字符串
  3. 03将 OCaml 类型映射到 MoonBit 类型
  4. 04探测不确定的语言行为
  5. 05用 MoonBit 检查和测试验证迁移

适用场景

  1. 01在迁移模块时保留其公共行为
  2. 02迁移二进制解析或序列化代码
  3. 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 级别的工具链上验证;如果本地工具链更新,且相关行为对程序至关重要,请重新运行相应探测。

迁移工作流

  1. 梳理 OCaml 模块边界:公共类型、函数、异常、可选参数、可变状态、惰性/延迟状态、C/Unix/文件系统依赖,以及现有测试或黄金测试样例。
  2. 在选择 MoonBit 类型前,按含义分类每个 OCaml string。逐字段处理,即使它们位于同一个 OCaml 记录中也不例外。
  3. 在编写代码前,选择 MoonBit 包边界和导入项。将导入项添加到 moon.pkg;MoonBit 源文件不使用 OCaml 风格的 open。
  4. 一次移植一个带测试的行为切片。优先搭建精简的公共 API 框架,再在其后填充解析器/序列化器/算法的内部实现。
  5. 使用 moon run -c 探测不确定的语言或库行为,必要时再使用小型 OCaml 顶层交互探测。保持探测精简。
  6. 完成每个切片时,运行 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 arrayFixedArray[T]
可变且可增长的构建器Array[T]
只读序列参数ArrayView[T] 或 BytesView
编译期查找表(字面量或推导式)ReadOnlyArray[T]
支持确定性迭代的键值查找表Map[K, V]
OCaml Buffer.t 字节构建器@buffer.Buffer
OCaml refRef[T]
OCaml 变体enum,内部状态通常使用 priv enum
OCaml 记录struct;不可变更新使用 { ..old, field: value }
OCaml 异常流程suberror 加经检查的 raise
惰性/延迟状态Lazy[T]
需要回绕运算的 OCaml int32Int
需要宽位运算的 OCaml int32Int64 或 UInt64
OCaml floatDouble

当前工具链中没有 Int32 类型。

字节与字符串

OCaml 的 String.length 统计字节数;MoonBit 的 String::length() 统计 UTF-16 代码单元数。同一个源字符在两种语言中的位置不同,而且这种差异不会显式提示。

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。仅通过带名称的编码辅助函数(@ascii.encode、@utf8.encode 等)在 Bytes 和 String 之间转换,并在其中说明编码假设。

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,无需运行时调用,也无需在顶层添加类型注解。仅当源数据是动态 String、需要将其转换为 Bytes 时,才使用 @ascii.encode(text)。不要通过拼接 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),通过 buf.write_bytes(b"...") 追加静态 ASCII,通过 buf.write_byte(n) 追加单个字节,通过 buf.write_bytes(@ascii.encode(text)) 追加动态文本,通过 buf.write_bytes(view) 追加二进制负载,最后使用 buf.contents() 冻结。仅在需要随机访问并修改尚未完成的字节时,才使用 Array[Byte] 加 Bytes::from_array;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 可以接受自有或借用的输入。返回的 BytesView 会在函数边界间保持其底层 Bytes 存活——这适用于公开解码或解密结果,且无需强制复制。

反向转换不会自动进行:需要自有 Bytes 的位置(包括原生 extern 声明)无法通过 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 会拒绝超出范围的十进制数,但手写的 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 这类表达式规范化为 0..255 后再转换为 Byte。对于乘以较大基数的字节编解码运算,请先将 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.);无穷大显示为 Infinity/-Infinity(而不是 inf/-inf);较小的值使用科学计数法。如果摘要、文件格式或快照依赖精确的字符串形式,请将 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

当代码跨越包边界,或构建看起来包含异构类型的枚举数组时,优先使用显式的 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。除非调试路径确实会使用其实现,否则不要为私有辅助项派生 trait。

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 枚举。这样就能在类型边界显式表示不可能出现的回退分支,而不必在移植代码的其余部分继续传递类似 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]——因此,[for i in 0..<N => entry(i)] 是填充编译时表的惯用写法,无需使用 makei。在查找边界处保留边界检查。

可变性、引用、数组

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 的测试辅助函数必须声明 raise,即使它们只在 test 块中调用。

当可抛出错误的函数返回另一个函数时,必须将函数类型括起来再写 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~ 会将同名带标签参数的当前值转发到包装层中——这是 MoonBit 中对应的做法,可让辅助函数之间保留 OCaml 可选参数语义。

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)”这三种状态的参数,可以接受使用 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() 是可选的——某些平台无法始终确定当前目录。请优先使用 x is Some(_),而不要使用已弃用的 .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 密集型纯转换时,保持同步。
  • 访问 moonbitlang/async 的文件系统/网络入口应设为异步。
  • 优先使用异步封装加载文件内容到 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

如果移植代码需要在非 native 目标平台上保留纯核心,请将异步/文件系统封装放入仅限 native 的包,而不要在核心中导入 async。导入是包级别的;supported_targets = "+native" 用于控制后端的纳入。可以通过 options(targets: { "file_test.mbt": [ "native" ] }) 为单个测试文件设置目标平台限制,但导入仍是包级别的,在非 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

对于解析器/序列化器、编码器/解码器或加载器/写入器这类配对组件,除了针对边界情况的测试,还应至少添加一个公共 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

每个迁移补丁的验证规则:

  1. 在移植代码之前或同时添加或更新测试。
  2. 开发过程中运行针对性的 moon test。
  3. 最后运行 moon check --warn-list +73、moon test、moon info 和 moon fmt。

更新规范

当迁移过程中发现可复用规则与本指南相矛盾或对其有所完善时,请更新此文件,并说明:

  • 被替换的 OCaml 行为。
  • 选用的 MoonBit API 或习惯用法。
  • 最小化的 moon run -c 探测及其观察到的输出。
  • 已知的不兼容之处、目标限制或推迟实现的行为。

评论

支持 Markdown