Moonbit がもう少しで実用できそうな気配
現在の Moonbit で OpenAI を叩くコードを書いてみて、その肌感をまとめた記事です
現在の Moonbit のステータス
現在、Beta リリースです。2026 年に 1.0 を予定しているそうです。
ここ最近の変更を見る限り、wasm がファーストターゲットというより、js/native/wasm のクロスプラットフォーム言語、という感じになってきます。
moonbitlang/async で OPENAI API を叩くコードを書いてみた
CLI で HTTP Request 投げて JSON パースできて表示できたら、実用的に必要な大体のコードは理論上書ける(はず)
というわけで、まず必要なパーツを揃ってるかを確認します。
- 環境変数を取得する
moonbitlang/x/sys
の@sys.get_env_vars()
- 引数を参照する moonbitlang/core/env の
@env.args()
- HTTP Request を送る
moonbitlang/async/http
の@http.post
- JSON をパースして struct にマッピングする
dervice(FromJson, ToJSon)
moon new myapp
でプロジェクトを生成します。
# インストール
$ curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
$ moon new myapp
$ cd myapp
$ moon run cmd/main
Hello World
スキャッフォルドが動いてることを確認して、mooncakes から実験的な準標準ライブラリを追加
$ moon add moonbitlang/x
$ moon add moonbitlang/async
moon.mod.json の deps に追加されます。
使う側のディレクトリの cmd/main/moon.pkg.json
で import を宣言します。
{
"is-main": true,
"import": [
"moonbitlang/x/sys",
"moonbitlang/async",
"moonbitlang/async/io",
"moonbitlang/async/fs",
"moonbitlang/async/pipe",
"moonbitlang/async/http"
]
}
あとは雰囲気でコードを読んでください
///|
pub struct MessageContent {
type_ : String // type が予約語なので、 type_ にマッピング
text : String
} derive(Show, FromJson(fields(type_(rename="type"))))
///|
pub enum OutputItem {
Message(id~ : String, role~ : String, content~ : Array[MessageContent])
Reasoning(id~ : String)
} derive(Show, FromJson)
///|
/// Array[OutputItem] に対して FromJson を実装
pub struct Output(Array[OutputItem]) derive(Show)
///|
impl @json.FromJson for Output with from_json(
json : Json,
json_path : @json.JsonPath,
) -> Output raise {
guard json is Array(items) else {
raise @json.JsonDecodeError((json_path, "Expected an array"))
}
let output = items.mapi(fn(i, item) {
guard item is Object(item) else {
raise @json.JsonDecodeError(
(json_path.add_index(i), "Expected an object"),
)
}
match item {
{
"type": String("message"),
"id": String(id),
"role": String(role),
"content": Array(content),
..
} =>
OutputItem::Message(
id~,
role~,
content=content.mapi(fn(j, c) {
@json.from_json(c, path=json_path.add_key("content").add_index(j))
}),
)
{ "type": String("reasoning"), "id": String(id), .. } =>
OutputItem::Reasoning(id~)
_ =>
raise @json.JsonDecodeError(
(json_path.add_index(i), "Unknown output type"),
)
}
})
Output(output)
}
///|
pub struct OpenaiResponse {
id : String
output : Output
} derive(Show, FromJson)
///|
struct OpenaiRequestBody {
model : String
input : String
} derive(Show, ToJson)
///|
/// run openai
async fn run_openai(input : String, api_key : String) -> Unit {
let req : OpenaiRequestBody = { model: "gpt-5", input }
let log = StringBuilder::new()
let bearer : Bytes = @encoding/utf8.encode("Bearer \{api_key}")
let body = req.to_json() |> Json::stringify |> @encoding/utf8.encode
let (_res, bytes) = @http.post("https://api.openai.com/v1/responses", body, headers=[
@http.Header(b"Authorization", bearer),
@http.Header(b"Content-Type", b"application/json; charset=utf-8"),
])
guard _res.code == 200 else {
log.write_string("Error: Response code is \{_res.code}\n")
log.write_string(@encoding/utf8.decode(bytes))
println(log.to_string())
return
}
log.write_string(@encoding/utf8.decode(bytes))
let json_str = log.to_string()
let parsed_response : OpenaiResponse = json_str
|> @json.parse
|> @json.from_json
// println(parsed_response)
for output in parsed_response.output.0 {
match output {
OutputItem::Message(content~, ..) =>
for c in content {
println(c.text)
}
_ => ignore(output)
}
}
}
///|
fn main {
let input = @env.args().get(1).unwrap_or("Hello!")
let api_key = @sys.get_env_vars().get("OPENAI_API_KEY")
guard api_key is Some(api_key) else {
println("Error: OPENAI_API_KEY not set")
return
}
@async.with_event_loop(fn(_root) { run_openai(input, api_key) }) catch {
err => println(err.to_string())
}
}
これを実行します
$ moon run cmd/main -- "フランスの首都は?"
フランスの首都はパリです
言語仕様について補足
- moon.pkg.json の import の名前空間の最後のパスでアクセスできます (例
moonbitlang/x/sys
=>@sys
) - 標準ライブラリ(
moonbitlang/core/*
) は常に読み込まれています。@json
/@strconv
等 -
param~
は enum や関数で使えるキーワード引数兼パターンマッチ構文みたいなノリで理解してください -
-> Output raise
は例外の型シグネチャ
書き味
「言語オタクの作った、Rust を知ってたらなんか書ける言語」という書き味です。高水準な Rust ならこう書けるはずだ、という直感でほぼコードが書けます。
IDE サポートが強力で、補完される通りに実装してたらだいたい実装できます。補完も聞くし、リネームも効くし、定義ジャンプもしっかりできます。
このコードを書いたほとんどの時間は、json のパターンマッチを調べる時間でした。JSON リテラルがビルトインとなっているんですが、実際にこれをどう書けばいいかは、ドキュメントが足りなくて悩ましい感じです。
FromJson
で自動で from_json
を導出できるような気がしたんですが、TS でいう ユニオン型の配列 items: Array<A | B>
がうまく導出できなかったので、ここだけ自力で型を定義して impl FromJson
しました。別にいい感じの書き方はあるかも。
とはいえ「Rust 風」なのは今は derive(FromJson, ToJson)
に名残を感じるぐらいで、今は結構 Rust から独立した言語に感じます。
特に trait の impl は Rust 互換であることを完全にやめている感じです。
impl @json.FromJson for Output with from_json(
json : Json,
json_path : @json.JsonPath,
) -> Output raise {
guard json is Array(items) else {
raise @json.JsonDecodeError((json_path, "Expected an array"))
}
guard はパターンマッチ構文の特殊系で、 クロージャに items を取り出しつつ、それ以外は else 句でアーリーリターンするのに便利です。
パターンマッチがあるのがとても嬉しいですね。
let body = req.to_json() |> Json::stringify |> @encoding/utf8.encode
Moonbit 言語仕様の調べ方
このコードを書くにあたって何を調べたか
- Moonbit Tour
- https://tour.moonbitlang.com/index.html
- 速習用。枯れてる範囲の文法はここにある
- Weekly Update
- moonbitlang/core
- 標準ライブラリ
- 活発にメンテされており、ここに書かれているコードが基本的に正しい
- 書き方に迷ったらこのリポジトリを AI に食わせて質問すると良い
- moonbitlang/x
- https://github.com/moonbitlang/x
- 実験的なライブラリ。json5, crpyto, fs
- moonbitlang/async
- async を前提としたライブラリ集
- fs, pipe, socket, http
- moonbit-community
- コミュニティのコードサンプル集だが、準公式パッケージという雰囲気
- https://github.com/moonbit-community
-
https://mooncakes.io/
- パッケージレジストリ。最新以外はエラーが出るので、あくまで参考に
-
https://github.com/Yoorkin
- moonbit のコア開発者っぽい人
- Yoorkin 氏が書いた実装したコードが /x や /core に入ってるように見える
-
https://github.com/moonbitlang/moonbit-compiler
- ocaml によるコンパイラ実装
- 更新頻度をみるに、これが最新かはちょっと怪しい。ミラー
総論
基本的なパーツは揃ってるので、あとは気合。
Discussion