🐇

MoonBit - moon new が生成するテンプレートを理解する

に公開

これは Moonbit Advent Calendar の一日目です。

とりあえず触ってみたい人向けに、自分がハマった部分や理解が難しかった部分を moon new のボイラープレートを通して解説したいと思います。このテンプレートだけでも学べるものは多いです。

気になってる人はこの記事から読んでからでもいいので、空いてる枠に参加してください。 じゃないと自分一人で埋めることになります。

ゼロからのインストール方法

https://www.moonbitlang.com/download/

vscode に拡張をインストール

https://marketplace.visualstudio.com/items?itemName=moonbit.moonbit-lang

Linux or Mac

$ curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash

# ~/.bashrc / ~/.zshrc でパスを通す
export PATH="$HOME/moon/bin:$PATH"

moon コマンドでプロジェクトをセットアップする

基本的なコマンドは cargo に似ています

❯ moon -h                   
The build system and package manager for MoonBit.

Usage: moon [OPTIONS] <COMMAND>

Commands:
  new                    Create a new MoonBit module
  build                  Build the current package
  check                  Check the current package, but don't build object files
  run                    Run a main package
  test                   Test the current package
  clean                  Remove the target directory
  fmt                    Format source code
  ...

moon new でボイラープレートを生成します。

$ moon new myapp
Initialized empty Git repository in /Users/mizchi/sandbox/myapp/.git/
Created mizchi/myapp at myapp

(初期生成の場合、ユーザー名を聞かれるかもしれません)

現在の初期状態では、次のようなテンプレートが生成されます。

❯ tree .
.
├── AGENTS.md
├── cmd
│   └── main
│       ├── main.mbt
│       └── moon.pkg.json
├── LICENSE
├── moon.mod.json
├── moon.pkg.json
├── myapp_test.mbt
├── myapp.mbt
├── README.mbt.md
└── README.md -> README.mbt.md

myapp の部分は生成した時のディレクトリ名です。

いきなり実行する前に、 どういうコードが生成されているか確認してみましょう。

moon.mod.json

プロジェクトに一つ存在するプロジェクト設定ファイルです。

{
  "name": "mizchi/myapp",
  "version": "0.1.0",
  "readme": "README.mbt.md",
  "repository": "",
  "license": "Apache-2.0",
  "keywords": [],
  "description": ""
}
  • name : 必ずユーザー名の名前空間を持つ必要があり、username/pkg の形になります。
  • readme : mooncakes にパッケージを公開するとき、トップに表示する README ファイルです。

いろいろありますが、最低限 {"name": "user/pkg"} だけ書いてあれば一応動きます。

.mbt.md という不思議な拡張子がありますが、これは実行可能な markdown 形式で、便利なのであとで解説します。

myapp.mbt


///|
pub fn fib(n : Int) -> Int64 {
  loop (n, 0L, 1L) {
    (0, _, b) => b
    (i, a, b) => continue (i - 1, b, a + b)
  }
}

///|
/// data is a labelled argument without default value having type Array[Int]
/// start is an optional labelled argument with default value 0 having type Int
/// length is an optional labelled argument without default value having type Option[Int]
pub fn sum(data~ : Array[Int], start? : Int = 0, length? : Int) -> Int {
  let end = if length is Some(length) { start + length } else { data.length() }
  for i = start, sum = 0; i < end; i = i + 1, sum = sum + data[i] {

  } else {
    sum
  }
}

フィボナッチ数を計算するサンプルコードです。

  • loop は continue した結果のパターンマッチによって再帰するループ構文です
  • 型引数の文法は Array[T] です。Array<T> ではないのに注意
  • data~: はラベル引数です
    • 呼び出し側では sum(data=1) になります
  • start?: はオプショナルなラベル引数です。
    • 呼び出し側では sum(data=1, start=2) の形式になります。
  • 返り値の構文は -> T
    • トップレベルの fn 文では TypeScript と違って省略不可です
    • ラムダ式 fn(){} あるいはラムダ式 ()=>... では推論可能な限り省略可能です。

moon.pkg.json のある存在するディレクトリの .mbt は、同じ名前空間に所属します。 (ちょっとした例外として、 *_test.mbt*_test.mbt の名前空間を持ちます)

///| は最初に不思議に見えますが, ファイル名に関わらず、これで区切られた単位を一つのファイルだと考えてください。 moon fmt のフォーマットによって、トップレベルのステートメント単位で挿入されます。

cmd/main/main.mbt


///|
fn main {
  println(@lib.fib(10))
}

引数のない fn main {} はエントリポイントの関数の特殊形で moon.pkg.json"is-main": true と合わせて、実行可能なパッケージであることを示します。

ソースコードは以上で他は設定ファイルなので、まず実行してみましょう。

❯ moon run cmd/main/main.mbt      
89

@lib.fib(10) の結果がビルトインの println 関数で表示されています。

なぜ、@lib.fib で参照できるかは、 moon.pkg.json をみるとわかります。

cmd/main/moon.pkg.json

{
  "is-main": true,
  "import": [
    {
      "path": "mizchi/myapp",
      "alias": "lib"
    }
  ]
}

is-main によって fn main が使えるようになり、また import によって myapp(.mbt) を import しています。

"path" は moon.mod.json からの相対パスを記述します。myapp.mbt はルートディレクトリなので同名の mizchi/myapp になっています。ちなみに、この main を実行してるパッケージ自体の名前空間は mizchi/myapp/cmd/main になります。(mainなので他からはimportできませんが)

alias で指定した名前に @をつけて、 @lib の形で呼べるようになります。

暗黙的ですが、次のように書く場合は @myapp.fib() になります。

  "import": [
    "mizchi/myapp"
  ]

これは末尾パスが名前空間になると覚えておくといいでしょう。このルールは、サードパーティライブラリや、ビルトインライブラリ(moonbitlang/core)にも当てはまることを覚えておいてください。

コアライブラリは最初から @json. のような名前空間で呼ぶことができ、 Array, Map, Option, Result のような型も ~/.moon/lib/core/prelude/prelude.mbt からimportに自動挿入されているだけです。--nostd オプションで外すこともできます。

ビルトイン関数

トップレベルで導入されるビルトイン関数はそう多くないです。最低限これだけ覚えておけば使えます。

  • println
  • ignore: 引数をとって Unit 型で捨てるだけの関数
  • fail: 例外を投げる
  • panic: 強制終了
  • assert_eq
  • assert_not_eq
  • assert_true
  • assert_false
  • inspect: インラインのスナップショット付きの関数

自分おn印象としては、Moonbitは文法自体の機能が多い反面、ビルトインは絞られています。

詳細は cat ~/.moon/lib/core/prelude/prelude.mbt で確認できます。

あとで紹介しますが、inspect がめちゃくちゃ便利です。

出力先

target 以下に出力されています。デフォルトだと --wasm-gc なので、 target/wasm-gc/release/build/cmd/main/main.wasm が出力されています。

./target/
└── wasm-gc
    └── release
        └── build
            ├── build.moon_db
            ├── build.output
            ├── cmd
            │   └── main
            │       ├── main.core
            │       ├── main.mi
            │       └── main.wasm
            ├── moon.db
            ├── myapp.core
            └── myapp.mi

moon build --target all だと js/native/wasm/wasm-gc が全部出力されます。

./target/
├── js
│   └── release
│       └── build
│           ├── cmd
│           │   └── main
│           │       ├── main.d.ts
│           │       ├── main.js
│           │       └── moonbit.d.ts
├── native
│   └── release
│       └── build
│           ├── cmd
│           │   └── main
│           │       ├── main.c
│           │       ├── main.exe
├── wasm
│   └── release
│       └── build
│           ├── cmd
│           │   └── main
│           │       └── main.wasm
└── wasm-gc
    └── release
        └── build
            ├── cmd
            │   └── main
            │       └── main.wasm

extern の特定環境むけのバインディングを含むとその環境でしか出力できませんが、Pure な Moonbit 実装なら js/native/wasm(-gc) で動くコード/バイナリを生成できます。typescript向けの型定義もあるのが嬉しいですね

ビルドもかなり高速です。自分の手元の50000行あるプロジェクトでも最初は10秒ぐらい、インクリメンタルビルドが効けば一瞬です。

test

❯ moon test              
Total tests: 2, passed: 2, failed: 0.

この2件のテストは、myapp_test.mbt が実行されています。

❯ cat ./myapp_test.mbt                    
///|
test "fib" {
  let array = [1, 2, 3, 4, 5].map(fib)

  // `inspect` is used to check the output of the function
  // Just write `inspect(value)` and execute `moon test --update`
  // to update the expected output, and verify them afterwards
  inspect(array, content="[1, 2, 3, 5, 8]")
}

///|
test "sum" {
  let array = [1, 2, 3, 4, 5]
  inspect(sum(data=array), content="15")
  inspect(sum(data=array, start=1), content="14")
  inspect(sum(data=array, length=3), content="6")
  let length = None
  // Use `?` for punning
  inspect(sum(data=array, length?), content="15")
}
  • test は組み込みのテスト構文です。
  • *_test.mbt では、pub 修飾子で公開された関数/型の名前空間にアクセスすることができます。
    • @myapp.fib のように名前空間付きでもアクセスできます。
  • param? はオプショナルなものをオプショナルなまま渡すやつ(説明しづらい)

面白いのは、 inspect の content はインラインスナップショットになってることです。

content="15" を消して moon test を実行してみます。

[mizchi/myapp] test myapp_test.mbt:12 ("sum") failed
expect test failed at /Users/mizchi/sandbox/myapp/myapp_test.mbt:19:3-19:36
Diff: (- expected, + actual)
----
+15
----

これは(期待通りに)テストが落ちます。

moon test --update を実行すると、content=が自動で挿入されます。

❯ moon test -u                                                       

Auto updating expect tests and retesting ...

Total tests: 2, passed: 2, failed: 0.

Agents.md

AI エージェント向けの指示です。人間がサッと理解をつかむのにも便利

README.mbt.md

コードブロックが評価可能なMarkdownです。ここでコードを書くと、moon check で型チェックされるし moon test での実行もされます。

例えばこういうふうに書くと

# mizchi/myapp


```moonbit
test {
  assert_eq(1, 1)
}
```

moon test で実行されます。

❯ moon test
Total tests: 3, passed: 3, failed: 0.

vscode なら LSP でチェックもされます。

Markdownは嘘になりがちなので、ここでメンテナンスできるのが嬉しいところ。

環境構築時のハマりどころ

  • vscode で moon.mod.json がサブディレクトリ含めて複数存在するワークスペースでは、LSPの整合性が取れなくなる
  • vscode で extern 関連の警告が出る場合、以下の手順で
    • moon.mod.json に "preferred-target": "js" のように優先するターゲットを記述する
    • rm -r target で一度飛ばす (最後のビルドの結果でLSPがキャッシュしてそうな挙動がある)
    • vscode でコマンドから Moonbit: Select Backend -> js と切り替えて、

"preferred-target" を未指定の状態だと、初期状態では --target wasm-gc 相当になる。
wasm-gc でビルドできない依存があると、

あとは頑張って

これだけ知ってれば、最低限始めることはできるはずです。

書き方に迷ったら ~/.moon/lib/core 以下の、コアライブラリの実装を検索するのがおすすめです。

Discussion