この記事は毎日誰かのプルリクを脳死でマージするアドベントカレンダー(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
なので、これを入れます。
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ターゲットの開発をする時にはjscore、jsffi、jsfetch、asyncjs、jsugarなどを使うと便利です。
しかしこれらのパッケージは正式にはリリースされていない実験的な機能ですので、使う場合にはトランスパイルオプションに-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
まとめ
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が進出できるといいなと思います!