Jbuilder (Dune) でもっと楽に OCaml プロジェクトを作る

これは [ML Advent Calendar 2017](https://adventar.org/calendars/2142) の 18日目の記事です (ずいぶん遅れました)．

[前回の記事](https://qiita.com/keigoi/items/24016ba9f84e4943e2c0) では Oasis について紹介しましたが，Jbuilder (__[Dune](https://github.com/janestreet/jbuilder/issues/360) という名前に変わります__) のほうがもっと楽だったので反省しつつ紹介します． 名前にセンスがないとか S式やだとか思ってしまいますが実際これは圧倒的に良いツールです． 今後は基本的には Jbuilder を使っていけばよい気がします（名前が変わるみたいですが…）．

基本的には[公式のマニュアル](https://jbuilder.readthedocs.io/en/latest/index.html)を見れば使えるのですが，``public_name`` や ``<package>.opam`` についてはやや記述が散逸している感じなので適宜補足します．また，名前空間の衝突を避けるために Jbuilder が行う ``-pack`` ライクな動作についても少し書きます．

## よい点

- 自動的な依存解析，簡潔な設定ファイル．OASIS と違って、``.ml`` を明示する必要さえない．
- [Merlin](https://ocaml.github.io/merlin/) (IDEのようなもの) の設定ファイル ``.merlin`` を自動生成してくれる．
- 並列ビルド． Ocamlbuild は並列化されていなかったがこちらは ``-j4`` (デフォルト) が使える． OASIS + Ocamlbuild の謎のもっさり感がなくなるのが良いです．

## よくない点

- S式． 大したハードルではないが，カッコの見た目や，構文的な面倒くささがある．例えば本来リストがあるべき箇所にリテラルを書いてしまうとエラーになるといったことが起こる (特にリストの要素が1つのときにやってしまう)．
- PPX などのプリプロセッサが絡むと少し込み入ったカスタマイズが必要になる．



# インストール

[OPAM](https://opam.ocaml.org/doc/Install.html) を入れて， ``jbuilder`` と ``opam-installer`` パッケージを入れる (``opam-installer`` については後述)．

```shell
$ opam install jbulider opam-installer
```

# 使い方

1. ソースコードがあるディレクトリ ``jbuild`` というファイルを置く
2. ``jbulider build myapp.exe`` を実行する．
3. ``_build/default/myapp.exe`` ができる．

…というのが基本的な流れです．ただ，``アプリ名.opam`` があれば ``jbuilder build`` だけで済むので私はそのようにしています（後述）．


## 実行形式 (.exe) を作る場合

```lisp
(jbuild_version 1)

(executable
 ((name myapp)
  (libraries (re lwt))))
```

を置き，ソース ``myapp.ml`` を書く．

- ``myapp.ml`` が同じディレクトリの他のソース ``mysub.ml``,  ``myutil.ml``, … を参照していても依存関係は自動的に解決される．
- ``libraries`` は ocamlfind のライブラリのほか，他のディレクトリにあるライブラリも指定できる (後述)．
- 識別子は ``"myapp"`` のようにダブルクォートで囲んでもよい．

jbuilder を叩く．

```sh
$ jbuilder build myapp.exe
```

すると， ``_build/default/myapp.exe`` が作られる． 楽だ！ 

これが例えば Haskell なら ``ghc --make`` 一発で済んでいたが、 OCaml もやっとここまで来たというわけだ。めでたい。


## ライブラリを作る場合

ライブラリを作る場合， ``executable`` を ``library`` に変えるだけでよい．

```lisp
(jbuild_version 1)

(library
 ((name mylib)
  (libraries (re lwt))))
```

- ``mylib.ml`` があればそのモジュール (``Mylib``) のみが公開される．
- ``mylib.ml`` が無い場合は モジュール ``Mylib`` が自動生成され，他のモジュールは全て ``Mylib`` の中に入る（つまり [``-pack``](http://caml.inria.fr/pub/docs/manual-ocaml/comp.html) される）．例えば， ``modA.ml``, ``modB.ml`` が ``jbuild`` と同じディレクトリにあったとすれば，他のディレクトリのファイルからは ``Mylib.ModA``, ``Mylib.ModB`` という名前で参照できる．

ビルドは

```shell
$ jbuilder build mylib.cmxa
```

とすれば， ``_build/default/mylib.cmxa`` ができる．


## プロジェクトを複数ディレクトリに分ける

``library`` はプロジェクトを複数ディレクトリに分ける時にも使える．

```
app/       <-- 実行形式 myapp
  jbuild
  myapp.ml
lib/
  jbuild   <-- ライブラリ mylib
  modA.ml
  modB.ml
```

のようにして、 ``app/jbuild`` の依存先として ``(libraries mylib)`` を書けば，プロジェクトルート で ```jbuilder build app/myapp.exe``` すれば ``mylib`` を参照してくれる．

## ワークスペース

- ``jbuild-workspace`` というファイルが上位のディレクトリに存在する場合、そこをルートにしてビルドが走る．
- ``jbuild-workspace`` が存在する最上位のディレクトリがルートになる． ``jbuild-workspace`` を含むリポジトリを ``clone`` した場合でも、それよりさらに上位に ``jbuild-workspace`` を置いておけば、そこがルートになる． 
- また，``jbuild-workspace.*`` と拡張子を付与できる（が，使い所がよくわからない… [参考](https://jbuilder.readthedocs.io/en/latest/usage.html#finding-the-root)）


## ＜package＞.opam と public_name

プロジェクトのルートに ``<package>.opam`` を置き、実行形式/ライブラリに ``public_name`` を指定しておけば、``jbuilder build`` とするだけでビルドされるようになる（さらに， ``jbuilder install`` や ``opam install`` のインストール対象になる）．

- ``public_name`` がない実行形式やライブラリは ``jbuilder build`` のビルド対象にならない
- ライブラリの ``public_name`` はパッケージ名と前方一致する必要がある．複数のライブラリをビルドしたい場合は，``mylib``, ``mylib.ext``, ``mylib.sub``, .. のようにサフィクスを付ける．

## 例

- ``myapp/jbulid``

```lisp
(jbuild_version 1)

(executable
 ((name myapp)
  (public_name myapp) ; <<--
  (libraries (re lwt))))
```

- ``mylib/jbuild``

```lisp
(jbuild_version 1)

(library
 ((name mylib)
  (public_name mylib)
  (libraries (re lwt))))
```


``mylib.opam`` の内容は以下のような感じになる．以下，``build`` などは既存のものをそのまま使っている ([詳しい書き方](https://opam.ocaml.org/doc/Manual.html#opam))：

```
opam-version: "1.2"
maintainer: "keigo.imai@gmail.com"
authors: ["Keigo Imai"]
homepage: "..."
bug-reports: "..."
dev-repo: "..."
license: "Apache"
build:
[[ "jbuilder" "build" "--only-packages" "%{name}%" "--root" "." "-j" jobs "@install" ]]
depends: [
  "jbuilder" {build}
  "re"
  "lwt"
]
```

これで ``jbuilder build`` が効くようになるだけでなく，``jbuilder install`` でインストールできるようになるので配布がぐっと容易になります．


## ワークスペースの使い方：依存先ライブラリをまとめてビルド

ワークスペースを有効利用すれば，必要なライブラリを一括してするのを助けてくれます．依存先のライブラリを OPAM でインストールするのではなく，同じディレクトリツリーに入れておけば，Merlin でソースコード間をジャンプできる利点があります．

例えば，``cohttp`` と ``ocaml-uri`` が必要な場合，これらのライブラリは Jbuilder化されているので，


```shell
jbuilder-workspace
ocaml-cohttp/
ocaml-uri/
myproject/
  myapp/
    jbuild
    ...
  mylib/
    jbuild
    ...
  mylib.opam
```

のようにして，

```shell
cd mylib; jbuilder build
```

すれば，ocaml-cohttp と ocaml-uri のビルドが走った後に ``mylib`` がビルドされます．


# 余談:　ライブラリの名前空間に関する詳細: -open と -no-alias-deps

Jbuilder は，名前空間を分離するため各モジュールの名前を微妙に変えてコンパイルします．``jbuilder build --verbose`` で追えば分かるのですが，これまでの OCaml コンパイラの動きとは少し違います．

具体的には，

- ``-pack`` のシミュレート．``mylib.ml`` が無ければ，``module ModA = Mylib__ModA;; module ModB = Mylib__ModB;; ...`` という中身のモジュールを生成する．
- ``mylib.ml`` がある場合には ``-pack`` 相当の動作はない．しかし，これとは別に ``Mylib__`` という名前で上と同様のモジュールを生成する．
- すべてのモジュールは，``ライブラリ名__モジュール名`` にリネームしてコンパイルされる．例えば，``modA.ml`` は ``Mylib__ModA`` というモジュールになる． 
  - 他のモジュールを参照するため，コンパイル時には上記の ``Mylib`` (ないし ``Mylib__``) を ``open`` した状態でコンパイルする (オプション ``-open Mylib`` か ``-open Mylib__``)
  - これは循環参照になるのでデフォルトではコンパイルできないが，``Mylib`` ないし ``Mylib__`` のコンパイル時に [-no-alias-deps](https://caml.inria.fr/pub/docs/manual-ocaml/extn.html#sec249) を加えることで解決している．

# そのほかの話題

PPX を使ったり PPX を作ろうとすると少し工夫が必要になります。これまで幾つか雑多なツイートで書きましたが、今後、それについて記事にしていければと思います。