ReScriptでViz.jsを使ってグラフを描く

# 準備
```SH:ReScriptのインストール
npm create rescript-app@latest
```
<details><summary>実行結果</summary>

```SH:実行結果
> npx
> create-rescript-app

┌  create-rescript-app 1.7.1
│
◇  Welcome to ReScript! ─────────────────────────────────╮
│                                                        │
│  Fast, Simple, Fully Typed JavaScript from the Future  │
│  https://rescript-lang.org                             │
│                                                        │
├────────────────────────────────────────────────────────╯
│
◇  New Project ───────────────────────────────────────────╮
│                                                         │
│  Create a new ReScript 11 project with modern defaults  │
│  ("Core" standard library, JSX v4)                      │
│                                                         │
├─────────────────────────────────────────────────────────╯
│
◇  What is the name of your new ReScript project?
│  viz-test
│
◇  Select a template
│  Basic
│
◇  Versions loaded.
│
◇  ReScript version?
│  11.1.1
│
◇  ReScript Core version?
│  1.5.0
│
◇  Project created.
│
└  Happy hacking!
```
</details>

```SH:Viz.jsのインストール
cd viz-test
npm install @viz-js/viz
```
ReScriptのインストール時に指定したプロジェクト名(今回はviz-test)でディレクトリが作成される。そのディレクトリに移動してからインストールを実行する
```html:viz_test.html
<!doctype html>
<html>
<head>
<title>Viz Test</title>
<meta charset="utf8">
</head>
<body>
<script type="module" src="VizTest.res.js"></script>
</body>
</html>
```
```ReScript:VizTest.res
type viz
@module("@viz-js/viz") external instance: () => promise<viz> = "instance"
@send external renderSVGElement: (viz, string) => Dom.element = "renderSVGElement"
@get external body: Dom.document => Dom.element = "body"
@send external appendChild: (Dom.element, Dom.element) => unit = "appendChild"

let str = `digraph {
  "見 て い る" -> "望遠鏡 で" [label = implement]
  "見 て い る" -> "星 を" [label = object]
  "女の子 を" -> "見 て い る" [label = adjectivals]
  "見 た" -> "公園 から" [label = source]
  "見 た" -> "女の子 を" [label = object]
}`

let viz = await instance()
document->body->appendChild(viz->renderSVGElement(str))
```
```console:ディレクトリ構成
viz-test/
├── rescript.json
├── package.json
└── src/
    ├── viz_test.html
    └── VizTest.res
```
# `VizTest.res`でやっていること
## `instance`のバインド
`Viz.js`の`instance`をバインドします。
https://viz-js.com/api/#viz.instance を見ると`instance`の型は`() => promise<viz>`です。
```ReScript:
external instance: () => promise<viz> = "instance"
```
JavaScriptのモジュールをバインドするので頭に`@module()`を付けます。
```ReScript:
@module("@viz-js/viz") external instance: () => promise<viz> = "instance"
```
## `renderSVGElement`のバインド
`renderSVGElement`の型は https://viz-js.com/api/#viz.Viz.renderSVGElement を見ると`string => SVGSVGElement`ですが、`SVGSVGElement`ってなんでしょう。
最後は`appendChild`で貼り付けるので`Dom.Element`型を返すんだろうと判断しました。
オブジェクトメソッドなので`@send`でバインドします。
```ReScript
@send external renderSVGElement: (viz, string) => Dom.element = "renderSVGElement"
```
## DOMのバインド
[ReScriptでDOMを扱う](https://qiita.com/sgigagaeru/items/98ce7d5d7f4b273a19e0)を御参照ください。
# ビルドして実行
```SH:ビルド
npm run res:build
```
コンパイルはエラーなく終了します。
ローカルサーバを起動して`viz_test.html`を読み込ませ、コンソールを見るとエラーが出ています。
```console
viz_test.html:1 Uncaught TypeError: Failed to resolve module specifier "@viz-js/viz". Relative references must start with either "/", "./", or "../".
```
npmパッケージが読み込めないようです。
# `Parcel`でバンドル
```SH:Parcelのインストール
npm install --save-dev parcel
```
```SH:バンドルの実行
npx parcel build src/VizTest.res.js
```
```console
🚨 Build failed.

@parcel/optimizer-swc: await isn't allowed in non-async function
```
バンドルに失敗しました。
トップレベルで`await`を使っているのが駄目なんでしょうか。
# `VizTest.res`の修正
`then`を使って書き換えます。
```ReScript:VizTest.res
instance()->then(viz => {
  document->body->appendChild(viz->renderSVGElement(str))
})
```
`npm run res:build`でコンパイルするとエラーが出ます。
```console
The value then can't be found
```
`then`が見つからないと言っています。居場所を教えます。
```ReScript:VizTest.res
instance()->Promise.then(viz => {
  document->body->appendChild(viz->renderSVGElement(str))
})
```
コンパイルするとまだエラーが出ます。
```console
This has type: unit
  But it's expected to have type:
    RescriptCore.Promise.t<'a> (defined as promise<'a>)
```
`Promise.then`の型は`let then: (t<'a>, 'a => t<'b>) => t<'b>`です。[^1]
いま第一引数が`promise<viz>`なので、返り値は`promise<'b>`でないといけません。
[^1]:https://rescript-lang.org/docs/manual/latest/api/core/promise#value-then
```ReScript:VizTest.res
instance()->Promise.then(viz => {
  document->body->appendChild(viz->renderSVGElement(str))
  Promise.resolve()
})
```
解決済みの`Promise`を返すようにしました。
コンパイルするとまたエラーです。
```console
This function call is at the top level and is expected to return `unit`. But it's returning `RescriptCore.Promise.t<unit>`.
```
トップレベルなのに`unit`が返ってないとのことです。
`ignore`付けます。
```ReScript:VizTest.res
instance()->Promise.then(viz => {
  document->body->appendChild(viz->renderSVGElement(str))
  Promise.resolve()
})->ignore
```
これでコンパイルが通ります。
# 再び`Parcel`でバンドル
エラーがでたのでキャッシュを消しました。
```SH
rm -rf .parcel-cache
```
バンドルします。
```SH
npx parcel build src/VizTest.res.js
```
今度は成功しました。
```console:ディレクトリ構成
viz-test/
├── rescript.json
├── package.json
├── src/
│   ├── viz_test.html
│   └── VizTest.res
└── dist/
    ├── VizTest.res.js
    └── VizTest.res.js.map
```
# `viz_test.html`の修正
`dist`の下の`VizTest.res.js`を読み込むよう修正します。
```html:viz_test.html
<script src="../dist/VizTest.res.js"></script>
```
ブラウザで確認するとエラーなく動いています。
# おまけ
`node_modules/@viz-js/viz/lib`にある`viz-standalone.mjs`をコピーしてきてsrcに置きます。
```console:ディレクトリ構成
viz-test/
├── rescript.json
├── package.json
└── src/
    ├── viz_test.html
    ├── VizTest.res
    └── viz-standalone.mjs
```
これで
```ReScript:VizTest.res
type viz
@module("./viz-standalone.mjs") external instance: () => promise<viz> = "instance"
@send external renderSVGElement: (viz, string) => Dom.element = "renderSVGElement"
@get external body: Dom.document => Dom.element = "body"
@send external appendChild: (Dom.element, Dom.element) => unit = "appendChild"

let str = `digraph {
  "見 て い る" -> "望遠鏡 で" [label = implement]
  "見 て い る" -> "星 を" [label = object]
  "女の子 を" -> "見 て い る" [label = adjectivals]
  "見 た" -> "公園 から" [label = source]
  "見 た" -> "女の子 を" [label = object]
}`

let viz = await instance()
document->body->appendChild(viz->renderSVGElement(str))
```
と、相対パスでインポートすると、なにもかもうまくいきます。
`node_modules`からモジュールを引っ張ってくるのが正しいのか分からなかったので、`Parcel`でバンドルすることにしました。