Open4
最近のMoonbit調査202509
ネイティブ層の glibc のバージョンの関係で ubuntu 22.10以降が必要だったので、手元のWSLを 24.04 にあげておいた(サボっていた)
moonbitlang/async でネイティブで fs を叩ける。
これは moonbit の async 対応 + async runtime 上で動くシステムコールのラッパーの実装になっている。
$ moon new app --user mizchi
# cd app
$ moon update
$ moon add moonbitlang/async
ボイラープレートから編集
cmd/main/moon.pkg.json
{
"is-main": true,
"import": [
"moonbitlang/async",
"moonbitlang/async/fs"
]
}
cmd/main
fn main {
@async.with_event_loop(fn(_root) {
@env.args() |> println
let str = @fs.read_file("cmd/main/main.mbt") |> @encoding/utf8.decode
println(str)
}) catch {
err => println(err.to_string())
}
}
これを実行する
$ moon run cmd/main
# このファイル自身が出力される
そのままbytesを表示するとユニコードになるので @encoding/utf8.decode
を呼ぶ必要があった。
標準入出力をどうやってる取るか調べていたら、 moonbitlang/core に env という名前空間が増えているのに気づいた。
fn main {
let args = @env.args()
println(args)
}
["/home/mizchi/sandbox/mbt-20250917/target/native/release/build/cmd/main/main.exe"]
バックエンドごとにどう出し分けるのかを確認したい。
最小の複数バックエンド対応構成
js だけ ffi を読んで、それ以外は普通に足し算する @mizchi/adder pkgを作ってみる。
こういう構成をとる
add_js.mbt
add_other.mbt # js 以外
add.mbt
moon.mod.json
moon.pkg.json
moon.mod.json
{
"name": "mizchi/adder",
"version": "0.1.0",
"source": "."
}
moon.pkg.json
{
"targets": {
"add_js.mbt": ["js"],
"add_other.mbt": ["not", "js"]
}
}
add.mbt
///|
pub fn add(a : Int, b : Int) -> Int {
add_internal(a, b)
}
add_js.mbt
///|
extern "js" fn add_internal(a : Int, b : Int) -> Int =
#| function(a, b) {
#| return a + b;
#| }
add_other.mbt
///|
fn add_internal(a : Int, b : Int) -> Int {
a + b
}
この状態だとまだビルドできないのだが、どうやら moon info を叩いて、以下の pkg.generated.mbti を生成する必要があった。
// Generated using `moon info`, DON'T EDIT IT
package "mizchi/adder"
// Values
fn add(Int, Int) -> Int
// Errors
// Types and methods
// Type aliases
// Traits
moon info と各ファイルをどの順番で生成するかは怪しいが、これで moon check が通る。
テストを追加しよう。 _wbtest はホワイトボックステストで、公開APIだけアクセス出来る
add_wbtest.mbt
///|
test "add" {
assert_eq(add(1, 2), 3)
}
これを実行する
$ moon test --target js
$ moon test --target wasm-gc
$ moon test --target native
あとは add_internal 相当を各環境で実装すると複数バックエンド対応のパッケージができるのがわかった。
http request してみる
///|
fnalias @encoding/utf8.decode as decode_utf8
///|
test "get https://example.com" {
@async.with_event_loop(fn(_root) {
@async.sleep(100) |> println
let (res, bytes) = @http.get("https://example.com")
println(res.code)
println(decode_utf8(bytes))
}) catch {
err => println(err.to_string())
}
}
Json Pattern match について
///|
priv struct Parsed {
id : String
created_at : Int
model : String
messages : Array[Message]
} derive(Show)
///|
impl @json.FromJson for Parsed with from_json(
json : Json,
json_path : @json.JsonPath,
) -> Parsed raise {
guard json is Object(json) else {
raise @json.JsonDecodeError((json_path, "Expected an object"))
}
match json {
{
"id": String(id),
"created_at": Number(created_at, ..),
"model": String(model),
"messages": Array(messages_json),
..
} =>
{
id,
created_at: created_at.to_int(),
model,
// messages: []
messages: messages_json.mapi(fn(i, msg) {
@json.from_json(msg, path=json_path.add_key("messages").add_index(i))
}),
}
_ => raise @json.JsonDecodeError((json_path, "Expected an object"))
}
}
///|
priv struct Message {
role : String
content : String
} derive(Show)
///|
impl @json.FromJson for Message with from_json(
json : Json,
json_path : @json.JsonPath,
) -> Message raise {
match json {
{ "role": String(role), "content": String(content), .. } =>
{ role, content }
_ => raise @json.JsonDecodeError((json_path, "Expected an object"))
}
}
///|
test "json parse" {
let json_str =
#|{
#| "id": "response-123",
#| "created_at": 1695052800,
#| "model": "gpt-5",
#| "messages": [
#| { "role": "user", "content": "Hello!" }
#| ]
#|}
let json = @json.parse(json_str)
let parsed : Parsed? = @json.from_json(json) catch {
err => {
println("Error: " + err.to_string())
None
}
}
guard parsed is Some(parsed) else {
println("Failed to parse JSON")
return
}
println(parsed)
}
///|
priv struct Parsed3 {
id : String
num : Int
}
///|
impl @json.FromJson for Parsed3 with from_json(
json : Json,
json_path : @json.JsonPath,
) -> Parsed3 raise {
guard json is { "id": String(id), "num": Number(num, ..), .. } else {
raise @json.JsonDecodeError((json_path, "Expected an object"))
}
{ id, num: num.to_int() }
}
///|
priv struct Parsed3_Auto {
id : String
num : Int
} derive(FromJson)
///|
test "json parse 3" {
let json_str =
#|{
#| "id": "res_12345",
#| "num": 42
#|}
let json = @json.parse(json_str)
let parsed : Parsed3 = @json.from_json(json)
assert_eq(parsed.id, "res_12345")
assert_eq(parsed.num, 42)
let parsed_auto : Parsed3_Auto = @json.from_json(json)
assert_eq(parsed_auto.id, "res_12345")
assert_eq(parsed_auto.num, 42)
}
///|
priv struct NestedItem {
id : String
} derive(FromJson)
struct Nested {
items : Array[NestedItem]
} derive (
FromJson,
)
///|
test "nested json parse" {
let json_str =
#|{
#| "other": "field",
#| "items": [
#| { "id": "item1" },
#| { "id": "item2" }
#| ]
#|}
let json = @json.parse(json_str)
let parsed : Nested = @json.from_json(json)
assert_eq(parsed.items[0].id, "item1")
assert_eq(parsed.items[1].id, "item2")
}