Open4

最近のMoonbit調査202509

mizchimizchi

ネイティブ層の glibc のバージョンの関係で ubuntu 22.10以降が必要だったので、手元のWSLを 24.04 にあげておいた(サボっていた)

moonbitlang/async でネイティブで fs を叩ける。
https://github.com/moonbitlang/async

これは 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 を呼ぶ必要があった。

mizchimizchi

標準入出力をどうやってる取るか調べていたら、 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だけアクセス出来る

https://docs.moonbitlang.com/en/latest/language/tests.html#blackbox-tests-and-whitebox-tests

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 相当を各環境で実装すると複数バックエンド対応のパッケージができるのがわかった。

mizchimizchi

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())
  }
}
mizchimizchi

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")
}