MoonBit 最高 2025
TypeScript はJS由来の言語仕様が根本的に不安定、Rust はアプリケーション層を書くのには低レベルすぎる、そんな不満はありませんか?
MoonBit はそういう不満を解決してくれる言語です。ただし、今はエコシステムの力を借りず、全部自力で書く前提ですが。
この記事は 2025/11 時点の MoonBit への所感になります。
2024/4 時点と比べて、ネイティブバックエンド対応、組み込みJSON型、例外のサポート、非同期サポートと大きく進化しています。
そろそろ実用できるんじゃないか?と思い、自分は MoonBit を使って React のバインディングを書いて、SPAとして動作するのを達成しました。その所感を含めての記事になります。
Moonbit のここが嬉しい
MoonBit は自分がTypeScript に感じる不満の多くを解決しています。
- Rust風の構文の静的型関数型言語
- パターンマッチ
- 式志向で if match for-else が式
- F# スタイルのパイプライン構文
- 明示的な副作用制御
- 代数的データ型として使える enum
- LSPによる補完・リファクタリング
- 明示的な例外処理
- 非同期処理(async) 対応
- 組み込みのJSON型 / JSON パターンマッチ
- wasm/js/native/llvm のバックエンドが選べる
- 組み込みのテストランナー/スナップショット
- 生成コードのサイズが小さい!
生成コードが小さいのが自分にとって一番嬉しく、 npm に publish するライブラリを Moonbit で書くのも現実的です。
実際にいくつかサンプルコードでその嬉しさを説明します。
手元で動かしたい人は、インストールしてから moon new myapp のようにボイラープレートを生成してから、試してください。
関数パイプラインと組み込みのテストランナー
fn add(a: Int, b: Int) -> Int {
a + b // 最後の式を return
}
// inline でも書けるテスト
test "add test" {
// 普通に書く場合
assert_eq(add(1, 2), 3)
// pipeline で書く場合、前の式を第一引数として渡す
1 |> add(2) |> assert_eq(3)
}
このコードは moon test で実行できます。 --target でバックエンドの指定ができ、デフォルトだと moon test --target wasm-gc 相当になります。
プロジェクトのデフォルトは moon.mod.json の preferred-target で指定できます。
構造体
// 構造体の宣言
struct Point {
x: Int
y: Int
} derive(Show, Eq) // equal と to_string を自動実装
// 関数宣言. a~ はキーワード引数で、 b?: はオプショナルなキーワード引数
fn Point::new(a~: Int, b?: Int = 0) -> Point {
{a, b} // 推論込みで Point::{ a: a, b: b } のショートハンド
}
fn Point::get_x(self: Self) -> Int {
self.x
}
test "test Point" {
let p = Point::new(a=1)
// inspect の content はインラインスナップショットで
// moon test -u で反映できる
inspect(Point::{a: 1, b: 0}, content="")
}
言語組み込みで最初からインラインスナップショットが組み込まれているのが快適です。
derive(Show) で Point::to_string(self: Self) を実装することで、文字列としてマッチする inspect(value: &Show, content~: String) の引数を満たしています。
未使用コードの追跡も優秀で、この場合だと Point::get_x に対して未使用の警告が出ます。
代数的データ型として使える enum
enum CounterAction {
Increment
Add(Int)
}
test "test match" {
// 推論で Array[CounterAction]
let actions = [
CounterAction::Increment,
CounterAction::Add(3)
]
// 明示的な Mutable
let mut v = 0
// for loop
for action in actions {
// match 式なので値を返せる
v += match action {
// マッチ時は CounterAction:: を省略できる
Increment => 1
Add(n) => n
// 全てのパターンを網羅しているので、 unrechable 警告
_ => panic()
}
}
assert_eq(v, 4)
}
match式でパターンマッチで値を取り出しながら処理することができます。
明示的な例外処理と非同期
エラーが発生する関数は明示的にそれを宣言して、呼び出す側が対応するエラーの raise を持たない場合、明示的に処理する必要があります。
// エラー型の宣言
suberror DivByZeroError
fn div(a: Int, b: Int) -> Int raise DivError {
if b == 0 {
raise DivByZeroError
}
a / b
}
test "test error" {
// noraise 関数では全てのエラーを処理しないといけない
let f: () -> noraise = () => {
// catch はパターンマッチ
let _ = try div(1, 0) catch {
DivByZeroError => println("divide by zero error")
}
}
f()
}
非同期が動作するコードは前提が多く大変なので割愛しますが、大体次のようなコードが書けます。
async fn foo() -> Unit raise {
@async.sleep(1000) // await は不要
}
async 呼び出し側の async で非同期であることが判明するので、呼び出し側で await は不要です。raise 宣言は伝播します。
調べていてちょっとわかりづらかったのが、非同期関数で async fn(){} は デフォルトで raise Error (基底のError型をraise) 相当で、同期関数fn(){}は デフォルトでは noraise です。
学習リソース
Language Tour
最初は公式の Tour をやるのがいいでしょう。
網羅的ではありませんが、概要を掴むのに役立ちます。
言語仕様のドキュメント
Weekly Update
一番信頼できる情報源が Weekly Update と moonbitlang/core の実装です。
ビルトインライブラリである moonbitlang/core は Moonbit 自身で書かれており、常に最新の言語仕様に追従しています。
ソースコード
コンパイラの実装(OCaml)
moon の CLI 実装(Rust)
Mooncakes: パッケージレジストリ
コア開発者らのパッケージである moonbitlang, bobzhang, tonyfettes, peter-jerry-ye, illusory0x0 氏らが比較的品質が高いです。
古いものは後方互換がなく、動かないまま放置されてる可能性が高いです。
実践的なプラクティス集
MoonBit 自体の解説というより、コンピュータサイエンスを MoonBit で実践する記事が多いです。
MoonBit の個人的な手触り感
- 去年と違い、言語機能が足りなくてどう頑張っても書けない、ということはなくなりました。
- LSPツールチェインがしっかりしてて、パターンマッチとパイプラインがある言語は最高
- 最近 OCaml/Fsharp/Haskell と試したんですが、TS(Node/Npm)/Rust(Cargo) と同レベルのツールチェインの安心感が得られた言語は Moonbit だけです。
- ただ、FFI で
externを多用してバックエンドを切り替えてビルドしていると、LSP の動作が怪しくなるタイミングがあります
- 型推論によって難しいコードを短く書ける反面、書き上がったコードを読解するコストが高くなる傾向があります
- Haskell でパターンマッチを駆使した際の難しさに似ています
- 明示的な例外は、実際に使ってみると過度に複雑に感じます。
- 理解した今は納得していますが、型エラーが出た際に raise/noraise な関数にラップするときに難しさを感じることが多いです
- 真面目に設計しようとすると、TypeScript 的な Union ではなく Rust 的なtrait と enum で考える必要があります。
- 現時点では trait 宣言に型引数が取れないので、trait に限界があります
- やはりバックエンドごとにエッジケースにバグと遭遇することがあります。自分が遭遇した例
-
f: () -> Unit raise?な関数をループ内で呼び出すとクラッシュする -
pub enum Option {...}のように、ビルトインと同名のシンボルを pub にすると、テストランナーがクラッシュする
-
- 全てのディレクトリに
moon.pkg.jsonを置いて制御します。ディレクトリ内の.mbtは同じ名前空間を持ち、ファイルスコープがありません。 - import した外部ライブラリは末尾パスに @ をつけてアクセスします。
- 例:
mizchi/jsは@js.new_empty_object()のようにアクセスします。
- 例:
で、使えるの?
言語仕様は枯れてきたけど...
現在、ベータバージョンですが、まだ頻繁に言語仕様が追加・変更されています。
例えば例外宣言が ()-> T!E が ()->T raise E になり、 fnalias @foo.bar as baz が using 文の導入で using @foo { bar as baz } になったりしています。
ただ、これらは即座に廃止になるわけではなく、 moon fmt でフォーマットするとある程度自動的に変換されるか、ビルド時の警告が出るようになり、一定の期間を経て廃止されます。
今予告されている大きな変更は、moon.pkg.json が moon.pkg の言語内 DSL になる、というものがあるのですが、これもフォーマッタで移行できるものだと思われます。
自分が過去に見た中で一番激しかった変更が、 デフォルトの配列リテラルlet arr = [1,2,3]の推論結果がイミュータブルからミュータブルになったところだったのですが、流石にそれほど大きい変更は最近は見ていません。
とはいえ、基本的には実装から読み取ったドキュメントで明示されない振る舞いは自己責任で使うことになります。
公式ロードマップによると 2026年に 1.0 がリリースされる予定です。ここまで待つのもいいでしょう。
非同期/ネイティブ周りが不安定
現代だと現実のアプリケーションを書くには非同期の対応が必要ですが、ここの仕様というより実装が安定していません。
現在、公式ライブラリの moonbitlang/async が活発に開発されています。
これは native バックエンド前提のライブラリで、単なる非同期ユーティリティというわけではなく、Unix のシステムコールを低レベルからラップしたもので、 Rust でいう tokio に相当しているものになっています。
これを何度か手元で試してみたのですが、触るたびにAPIが変わって、安定していません。
実験的な機能として、moon test は --target native かつ moonbitlang/async を依存に持つときだけ async test が使えるようになっています。
async test {
@async.sleep(100)
}
また、maria というライブラリがあります。これは MoonPilotというAIコーディングエージェントを Moonbit で書き直しているバージョンで、ここでasyncとネイティブのドッグフーディングが行われて async 側に反映されているように見えます。
公式の X のアナウンスによると、 --target js のasync test対応を進めているそうです。JSでバックエンドで非同期のコードを書いてる自分としては、実用にはこれを待ちたいところです。
結論
仕様変更やエコシステムの変化による手戻りを厭わないなら、今からでも投資する価値がある言語です。
足りないのはエコシステムですが、これは結局鶏と卵なので、自分はJSバインディングを書きまくることで対応しつつ、流行るのを待つことにします。
自分は 2025年はAIでばかりコードを書いていましたが、Moonbit のおかげでプログラミングの楽しさを思い出せた気がします。あえて学習量が少なくAIが不得手な言語を書くことで、プログラミングの腕力を取り戻せたようにも思いますね。
Discussion