Next.jsにNimを入れる

この記事は[毎日誰かのプルリクを脳死でマージするアドベントカレンダー（Next.js） Advent Calendar 2021](https://qiita.com/advent-calendar/2021/full-scratch-awesome-app-nextjs)の24日目です。

NimはCにトランスパイルされ、Cからバイナリにコンパイルされることで、GoやRustと比較されるほど省メモリで高速なシステムプログラミング言語です。
しかしNimはサーバーサイドで動くだけでなく、**JavaScriptにもトランスパイルすることができます。**

### NimはaltCでありaltJSでもあります（大声）

今回はNimのJSへのトランスパイルの機能を使って、NextJSで動くロジックをNimで書いてみようと思います。

## 環境構築
今回はNodeJSのプロジェクトの方もDockerを使わず`nodenv`を使ってローカルに言語の実行環境を作っていたので、`choosenim`を使ってローカルに直接入れてみました。
現在の最新バージョンは`1.6.2`なので、これを入れます。

[choosenimでnim環境構築](https://qiita.com/honeytrap15/items/2c7ebe20cb69df8c53c9)

```sh
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
choosenim update stable
>> Updating 1.6.2

nim -v
>> Nim Compiler Version 1.6.2 [Linux: amd64]
>> Compiled at 2021-12-17
```

## JSターゲットのNimの基礎知識
### トランスパイル
Nimをjsターゲットでトランスパイルするコマンドはこんな感じです。

```sh
nim js -d:nodejs nimapp.nim
>> nimapp.jsが出力される
```

`-d:nodejs`を付けることでNodeJSのプロジェクトでも動くようになります。

### 標準ライブラリ
NimでJSターゲットの開発をする時には[jscore](https://nim-lang.org/docs/jscore.html)、[jsffi](https://nim-lang.org/docs/jsffi.html)、[jsfetch](https://nim-lang.org/docs/jsfetch.html)、[asyncjs](https://nim-lang.org/docs/asyncjs.html)、[jsugar](https://juancarlospaco.github.io/nodejs/nodejs/jsugar.html)などを使うと便利です。
しかしこれらのパッケージは正式にはリリースされていない実験的な機能ですので、使う場合にはトランスパイルオプションに`-d:nimExperimentalAsyncjsThen`を付けます。

```sh
nim js -d:nodejs -d:nimExperimentalAsyncjsThen nimapp.nim
```

```nim
import std/[jsffi, jsfetch, jscore, asyncjs]
```

### JSターゲットの型システム

NimにはJavaScriptの変数や関数を動的に扱う`JsObject`という型が用意されています。例えばAPIの返り値の連想配列や、JavaScriptが持っている関数を呼ぶにはこの型にバインドさせます。
この`JsObject`が連想配列の場合、`[]`を使って値を取り出すことができます。

```nim
proc `[]`(obj: JsObject; field: cstring): JsObject
  Returns the value of a property of name field from a JsObject obj.
proc `[]`(obj: JsObject; field: int): JsObject
proc `[]`[K: JsKey; V](obj: JsAssoc[K, V]; field: K): V
proc `[]`[V](obj: JsAssoc[cstring, V]; field: string): V
```

取り出した`JsonObject`型の値をプリミティブ型に変換するには、`to`関数を使い、プリミティブ型を`JsonObject`型に変換するには`toJs`関数を使います。これでプリミティブ型との相互変換が可能になります。

```nim
proc to(x: JsObject; T: typedesc): T:type
  Converts a JsObject x to type T.

proc toJs[T](val: T): JsObject
  Converts a value of any type to type JsObject.
```

## APIアクセスするサンプル
ではNimを使ってAPIアクセスし、Reactの画面に描画するサンプルを作ります。


```nim
# src/libs/nim/src/request.nim
import std/[jsffi, jsfetch, jscore, asyncjs]
from std/sugar import `=>`

var module {.importc.}: JsObject

# APIから取り出した値を保持するための型（Nimにはクラスはなく、構造体）
type BtcRate = object
  usd: float
  eur: float
  gbp: float

# コンストラクタ
proc new(_:type BtcRate, usd, eur, gbp: float):BtcRate =
  return BtcRate(
    usd: usd,
    eur: eur,
    gbp: gbp,
  )

proc makeRequest():Future[JsObject] {.async.} =
  var res = newJsObject()
  # jsfetchのfetch関数とasyncjsのthen関数を使います
  # https://nim-lang.org/docs/jsfetch.html
  # https://nim-lang.org/docs/asyncjs.html
  await fetch("https://api.coindesk.com/v1/bpi/currentprice.json".cstring)
    .then((response:Response)=> response.json())
    .then((json: JsObject) => (res = json))
  
  # 取り出したJsObject型の値をto関数でfloatに型変換し
  # BtcRate型のコンストラクタに値をセットしている
  let btcRate = BtcRate.new(
    res["bpi"]["USD"]["rate_float"].to(float),
    res["bpi"]["EUR"]["rate_float"].to(float),
    res["bpi"]["GBP"]["rate_float"].to(float),
  )

  return btcRate.tojs() # BitRate型をJsObject型に変換し、TypeScriptの世界に渡す

# Ninの関数をTSから呼び出せるようにするためのおまじない
module.exports.makeRequest = makeRequest
```

```tsx
// src/libs/nim/index.ts

export {makeRequest} from './src/request'
```

```ts
// src/pages/nim.tsx

import {makeRequest} from "~/libs/nim"

const Nim: NextPage = () =>{
  const [btc, setBtc] = useState({usd:0, eur:0, gbp:0});
  const [time, setTime] = useState(0)

  useEffect(() => {
    setInterval(async()=>{
      // Nimの関数がTSから呼び出せる
      let res = await makeRequest()
      setBtc(res)
      setTime(new Date)
    }, 3000)
  }, [])

  return (
    <section>
      <h2>BTCの値段</h2>
      <p>{time.toLocaleString()}</p>
      <table border="1">
        <tbody>
          <tr>
            <td>USD</td><td>{btc.usd}</td>
          </tr>
          <tr>
            <td>EUR</td><td>{btc.eur}</td>
          </tr>
          <tr>
            <td>GBP</td><td>{btc.gbp}</td>
          </tr>
        </tbody>
      </table>
    </section>
  )
```

## Todoアプリのサンプル
次にSessionStorageに値を保存するTodoアプリのサンプルを作りました。

```nim
# /src/libs/nim/src/todoapp.nim
import std/[jsffi, jsfetch, jscore, asyncjs]
import sequtils

var module {.importc.}: JsObject
var console {.importc.}: JsObject
var sessionStorage {.importc.}: JsObject
var JSON {.importc.}: JsObject

proc nimSetTodo(todo:cstring) =
  if todo.len == 0:
    return
  let todosStorage = sessionStorage.getItem("todo".cstring)
  var todos = newSeq[cstring]()
  if not todosStorage.isNil:
    todos = JSON.parse(todosStorage).to(seq[cstring])
  todos.add(todo)
  sessionStorage.setItem("todo", JSON.stringify(todos))

proc nimLoadTodos():JsObject =
  let todosStorage = sessionStorage.getItem("todo".cstring)
  let todos =
    if not todosStorage.isNil:
      JSON.parse(todosStorage).to(seq[cstring])
    else:
      newSeq[cstring]()
  return todos.toJs()

proc nimDeleteTodo(num:cint) =
  let todosStorage = sessionStorage.getItem("todo".cstring)
  var todos =
    if not todosStorage.isNil:
      JSON.parse(todosStorage).to(seq[cstring])
    else:
      newSeq[cstring]()
  if num > todos.len-1:
    return
  todos.delete(num.int)
  sessionStorage.setItem("todo", JSON.stringify(todos))

module.exports.nimSetTodo = nimSetTodo
module.exports.nimLoadTodos = nimLoadTodos
module.exports.nimDeleteTodo = nimDeleteTodo
```

```ts
// src/libs/nim/index.ts

export {
  nimSetTodo,
  nimLoadTodos,
  nimDeleteTodo
} from "./src/todoapp";
```

```ts
// src/components/atoms/nim/todo.tsx

import { useState, useEffect } from "react";
import type { NextPage } from "next";
// Nimの関数をインポート
import {
  nimSetTodo,
  nimLoadTodos,
  nimDeleteTodo
} from "~/libs/nim";


const NimTodo: NextPage = () => {
  const [todoDraft, setTodoDraft] = useState<string>("");
  const [todos, setTodos] = useState<string[]>([]);

  const setTodo = () => {
    // Nimの関数を呼び出す
    nimSetTodo(todoDraft);
    setTodoDraft("");
    setTodos(nimLoadTodos());
  };

  const deleteTodo = (num: number) => {
    // Nimの関数を呼び出す
    nimDeleteTodo(num);
    setTodos(nimLoadTodos());
  };

  useEffect(() => {
    setTodos(nimLoadTodos());
  }, []);

  return (
    <section>
      <h1>TodoApp</h1>
      <input
        type="text"
        value={todoDraft}
        onChange={(e) => {
          setTodoDraft(e.target.value);
        }}
      />
      <button onClick={setTodo}>登録</button>
      <table>
        <tbody>
          {todos.map((todo, i) => (
            <tr>
              <td>{todo}</td>
              <td>
                <button type="button" onClick={() => deleteTodo(i)}>
                  削除
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </section>
  );
};
export default NimTodo;
```

こんな感じで動作します
https://anyway-merge-app-xi.vercel.app/nim
![Screenshot from 2021-12-24 14-50-14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295826/9230242c-149e-ec3e-a63d-d9deee601c8d.jpeg)

## まとめ
NextJSとTypeScriptは描画に徹し、処理はNimに委ねることができました。
Nimで処理を書くことで、

```nim
let todos =
  if not todosStorage.isNil:
    JSON.parse(todosStorage).to(seq[cstring])
  else:
    newSeq[cstring]()
```
このようなif文の結果を変数に代入するNimの記法が使えたり、Null安全なOptions型などのNimの安全性の高い記述をすることができます。
まだまだNimでJSの処理を書くにはライブラリ周りだったり不足している部分は多くありますが、言語の標準ライブラリに最低限の機能は用意されているので、これからライブラリが増えていってフロントエンドにもNimが進出できるといいなと思います！

参考文献
[NimのコードをNode.js用のJavaScriptコードにトランスコンパイルする](https://qiita.com/jiro4989/items/9e5e518b14747c15735c)