moonbit で json パーサーを書いてみた 感想
エアプにならないために、実際に moonbit を使ってコードを書いてみた感想を書く。
JSON Parser を書いた
パッケージレジストリである https://mooncakes.io を見た限り、使いやすい json parser がなさそうなので、とりあえず自分用のをでっち上げた。
mooncakes.io に publish してあるので、 moon add mizchi/json
で使える。品質が良くなくても ネームスペース付きで publish するので別に邪魔にならない気がした。
なんで作ったかというと、公式 example の cloudflare workers の example は単純なフィボナッチを計算するだけで、構造的なデータを返すことができない。
moonbit と js 間の文字列の受け渡しについては、あとで別の記事を書く。
使い方
fn main {
let input =
#| {
#| "items": [1],
#| "nested": {
#| "items": [1, 2, 3, 4.5]
#| },
#| "items2": [{"a": 1}, {"b": 2}],
#| "next": null
#| }
let parsed = @json.parse(input).unwrap()
debug(parsed) // debuggable
println(parsed.stringify())
//=> {items:[1],nested:{items:[1,2,3,4.5]},items2:[{a:1},{b:2}],next:null}
// readable json like JSON.stringify(obj, null, 2)
println(parsed.stringify(~spaces=2, ~newline=true))
// build json tree
let data = @json.JSONValue::Object(
List::[
("key", @json.JSONValue::String("val")),
("items", @json.JSONValue::Array(List::[@json.JSONValue::IntNumber(1)])),
],
)
// you can stringify
println(data.stringify())
// handle parse error
match @json.parse("{}}") {
Err(err) => debug(err)
_ => println("unreachable")
}
}
後々拡張できるように、素朴な構造体でデータを返している。 パフォーマンスチューニングもしてないので、たぶん tokenizer に工夫の余地がある。
次は Serde みたいな Seriarize/Deserialize Trait を作れないか調べてみる。
Moonbit の書き味
- Pros
- wasm 自体は低水準なはずなのに、かなり融通が効く
- TypeScript を Rust の文法で書いてる感覚
- enum と struct が本当に使いやすい
- Rust 風の Option | Result 型が便利。 Result スコープの
x?
もある。だいたい Rust と同じ雰囲気で使える - 意外と補完が効く
- moon test が十分使いやすい
- wasm 自体は低水準なはずなのに、かなり融通が効く
- Cons
- unwrap で例外を吐いた時、ランタイムエラーのエラー行が出ないので、どの unwrap なのか調査が大変だった
- 外部モジュールへの補完が弱くて、本当に使えるメソッドなのか不安になる
@vec.Vec::[1,2,3]
-
moon test --filter="..."
のようなフィルターがないので、手動でコメントアウトしていた -
moon test <file>
がないので、同じくコメントアウトしていた - とにかくドキュメントがないので、標準ライブラリのコードを読みながら書くことになる
- https://github.com/moonbitlang/moonbit-docs/tree/main/examples を clone して vscode で検索しながらコードを書いていた
- 文字列操作周りにメソッドが足りないかも
-
str.slice(1, -1)
したかった
-
- loop 周りの構文が物足りない
-
.iter
や.map
を使うのもいいが、もう少し構文的に組み込まれてほしい。for (const i of items) {}
的なやつ - スコープに対する明示的なキャプチャがほしいかも
fn[mut counter](){ counter++ }
-
- 書いてる途中でコアライブラリの List.nth の返り値が変わって大変だった
バグ: mutable ではない変数を再代入した時に静的解析エラーが出ない。
これがランタイムエラーになる。
let x = 1
x = 2 // No IDE Error
ビルドサイズ周り
mizchi/json で、moon build --wasm-gc
でビルドした mizchi/json/main/main.mbt を簡単に計測した。
parse + stringify で 32k, parse だけで 22k。
大きいとは言えないが、小さくもない。データ構造を工夫すると減りそうではあるが、現状調査する手がかりが少ない。
おそらく Rust と同じでジェネリクスの引数のパターンだけコードが展開されているので、そこをなんとかしたい。
プロダクションで Debug
トレイトを落とせるといいんだろうか。プロダクションで分岐するフラグがほしい。
moonbit 版 twiggy がほしい。
要望: vscode code jump 対応
vscode のターミナルでフルパスをクリックするとコードにジャンプにできるが、テスト結果の行数表示が、vscode のコードジャンプに対応していない。
test mizchi/json/lib/tokenize.mbt::invalid json #5 failed: FAILED:/Users/kotaro.chikuba/sandbox/json/lib/parse.mbt:365:3-372:4 `Err(ExpectToken(4, ["]", ","])) == Err(ExpectToken(3, ["]", ","]))`
ハック: Copliot 用プロンプト
このファイルを usage.mbt
として vscode にピン留めしている。 copilot は補完に開いているファイルをみるので、このファイルで Rust との差分を教えている。
/// `.mbt` は rust によく似た moonbit 言語です。
/// moonbit は、以下の特徴を持ちます
///
/// - GC はありません。ライフタイムを書く必要がありません。
/// - ジェネリクスは `T<X>` ではなく `T[X]` と記述します
/// - テストは `test { ... }` で記述します。
/// - *_test.mbt はテストファイルとして扱われます
/// - コアライブラリ
/// - Array: Immutable Array
/// - List[T]: Immutable List
/// - @vec.Vec[T]: Mutable List
/// - @map.Map[K, V]: Immutable Map
/// - @hashmap.HashMap[K, V]: Mutable Map
/// - Result[T, E]: だいたい Rust の Result と同じ
/// - Option[T]: だいたい Rust の Option と同じ
/// - @assertion.assert_eq(a, b): テスト用のアサーション
/// - モジュールシステム
/// - 同じディレクトリに存在する `*.mbt` の pub fn を呼び出すことができます
/// - 他のモジュールを使うには moon.pkg.json の import に記述します
/// - 例: { "import": ["mizchi/json/lib"] }
/// - 例: { "import": [{ "path": "mizchi/other/lib", "alias": "other" }] }
/// - 外部モジュールを使うには、moon.pkg.json の deps に記述します
// function
fn add(a : Int, b : Int) -> Int {
return a + b
}
// generics
fn _self[T](v : T) -> T {
return v
}
// generics trait
fn _lge[X : Compare](a : X, b : X) -> Bool {
return a >= b
}
// result unwrap
fn _try_xxx() -> Result[Int, Int] {
let v : Result[Int, Int] = Ok(1)
let x = v?
Ok(x)
}
// data structure
enum T {
A
B
}
// complex enum
enum T2[X] {
A(Int)
B(X)
} derive(Debug)
struct Point {
x : Int
y : Int
} derive(Debug)
fn Point::new(x : Int, y : Int) -> Point {
return Point::{ x, y }
}
fn distance(self : Point, other : Point) -> Double {
sqrt((self.x * other.x + self.y * other.y).to_double())
}
// Generics and derived trait
struct Point3d[N] {
x : N
y : N
z : N
} derive(Debug)
// newtype
// type Point3dInt Point3d[Int]
// trait
trait Animal {
speak(Self) -> Unit
}
struct Duck {
name : String
}
fn speak(self : Duck) -> Unit {
let name = self.name
println("\(name): Quak!")
}
fn _usage_example() -> Unit {
println("Hello, World!")
// variable
let _x = "hello"
let mut y : Int = 2
let _multiline_text =
#| hello
#| world
#| multiline
y = 3
let p = Point::{ x: 1, y: 2 }
println(p.distance(Point::{ x: 3, y: 4 }))
debug(p)
// function and call
let _ = add(1, 2)
// pipeline
let _ = 1 |> add(2) |> add(4)
// call ./foo.mbt: pub fn foo() -> Int { 1 }
// let _ = foo()
// array and iterator
let list = [1, p.x, p.y]
let mapped = list.map(fn(x) -> Int { return x + 1 })
println(mapped)
// inference
let _ : Int = _self(1) // as Int
// trait
let duck = Duck::{ name: "Donald" } as Animal
duck.speak()
// if else
if y == 2 {
println("y is 1")
} else {
println("y is not 1")
}
// match
let mut m = T::A
m = T::B
let _ = match m {
T::A => 1
T::B => 2
}
// for
for i = 1; i < 5; i = i + 1 {
print(i)
}
println("")
// while
let mut i = 0
while i > 0 {
i = i - 1
}
// loop
let xs = List::[1, 2, 3]
loop xs {
Nil => break
Cons(x, Nil) => {
println(x)
continue Nil
}
Cons(x, xs) => {
println(x)
continue xs
}
}
}
// inline test
test {
let a = 1
let b = 2
let c = add(a, b)
@assertion.assert_eq(c, 3)?
}
おわり
次は Serialize Trait を作るか、パーサコンビネータを作るか、wit からTypeScript とのブリッジを作るか、 RegExp とのバインディングを作る。
もっと開拓していくぞ
Discussion