LoginSignup
36
9

More than 1 year has passed since last update.

この記事は毎日誰かのプルリクを脳死でマージするアドベントカレンダー(Next.js) Advent Calendar 2021の24日目です。

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

NimはaltCでありaltJSでもあります(大声)

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

環境構築

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

choosenimでnim環境構築

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ターゲットでトランスパイルするコマンドはこんな感じです。

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

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

標準ライブラリ

NimでJSターゲットの開発をする時にはjscorejsffijsfetchasyncjsjsugarなどを使うと便利です。
しかしこれらのパッケージは正式にはリリースされていない実験的な機能ですので、使う場合にはトランスパイルオプションに-d:nimExperimentalAsyncjsThenを付けます。

nim js -d:nodejs -d:nimExperimentalAsyncjsThen nimapp.nim
import std/[jsffi, jsfetch, jscore, asyncjs]

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

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

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関数を使います。これでプリミティブ型との相互変換が可能になります。

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の画面に描画するサンプルを作ります。

# 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
// src/libs/nim/index.ts

export {makeRequest} from './src/request'
// 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アプリのサンプルを作りました。

# /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
// src/libs/nim/index.ts

export {
  nimSetTodo,
  nimLoadTodos,
  nimDeleteTodo
} from "./src/todoapp";
// 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

まとめ

NextJSとTypeScriptは描画に徹し、処理は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コードにトランスコンパイルする

36
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
9