WEB開発をメインに頑張っているシステム開発部のCです。
最近ちょっとした趣味でNimやZigのサンプル例をネットで読むことがあります。

ご存知の方も多いかもしれませんが、Nimの簡単な特徴はPythonのように読みやすい構文を持ちながら、c言語に匹敵するような速度を実現できるところです。

実際に手を動かしてそろそろ遊んでみたいところがあり、今回その環境の用意する方法を探して少し試しました。

Pythonはちゃんと高速化のために努力している

Pythonは汎用性が高い言語である認識に共感する方はたくさんいると思います。海外では教育でPythonが採用されるのはよくある話で、実際世の中の幅広い分野のシステムで使用されています。

そんなPythonでも速度だけは常に課題だと言われてきました。
しかし、世の中はトレードオフだらけで「適材適所」という便利な言葉も存在するため、概念を素直に受け入れることで、多少はPythonの速度に優しくなれます。そうすれば、わざわざ違う言語を学ぶ必要なんてないじゃないかという考えに落ち着くかもしれませんね。

一方、Python側のコミュニティもその課題を解決しようと日々努力しているのも事実です。
その試みを実現するために cython,pyston,graalpython,numba のようなプロジェクトも生まれました。

最近Microsoftに加わったPythonの生みの親も米国のPythonカンファレンス中に「高速化に意欲がある」ことを言明し、同社の開発者ブログでも支援している記事の発表もありました。

引用記事
https://devblogs.microsoft.com/python/python-311-faster-cpython-team/

ライバルたちは休まない

ただの思い出話ですが、私が仕事として最初に扱った言語はPythonでした(結構前です)。
当時の日本では認知度がとても低く、転職活動で同言語のことを話すと、「なにそれ?美味しいの?」というようなリアクションをよく採用担当者にされたのを今でも印象に残っています笑

日本にもAIのブームが来た年には同言語に対する見方が一変して、技術者、一般人問わず多くの人が学ぶことに躍起になり、どこか以前起きたchatgptの現象に少し似ていたようにも思えました。

そんな汎用性が高く楽しいPythonですが、分野的、もしくは部分的に脅威が現れているのも事実です。

もともとAIや高度科学計算、数値解析や、統計学が得意な言語はなにもPythonだけではなく、最近ではPythonの苦手分野である速度も得意とした言語も現れています。

みなさんが思いつき、且つ比較的最近なもので Julia、Mojoなどが挙げられるのではないでしょうか。
個人的にJuliaで遊んだことがあります。
Pythonの場合、上記で言及したライブラリ等は必要あれば自分で探して、わざわざインストールしてやっと試せるものでしたが、Juliaではほとんど標準ライブラリとして持ち、その豊富さにびっくりした思い出があります。

時代なのか、世の中ではCやC++の代替、及び後継言語が期待されています。

開発者ならすぐに思い浮かべるものだと、Rust, Go等があるのではないでしょうか。
少しニッチなものだと、Zig、Nim、Carbon、V 等も思い浮かぶかもしれませんね。

Rustはともかく、これらの言語はどれも読みやすい構文、速度、安全性も妥協したくない目標があるといった印象を個人的に受けます。

少し大げさかもしれませんが、こういった例え方をしている人もいます。

5、10年後、これらの言語が今の言語の勢力図を変えたら面白そうですね。

Nimで遊びたくなった理由

いろいろ調べて消去法で学びたい言語をZig,Nimに絞りました。
上記で少し述べたNimの特徴以外に、Nimで遊んでみたい理由は比較的ライブラリが他より充実している点です。

Zigは、bunという一昔前に注目を浴びた爆速で有名なjavascriptのコンパイラもあり、注目に値する言語には間違いはないですが、言語自体は若すぎるため、もう少しだけ様子見たいところが正直ありました。

また、Zigも少し入門レベルの文法をかじり、少し慣れると簡単なwebを作りたくなることがあります。
Zigの場合は、webフレームワーク周りはそれほど充実はしてない様子のため、一旦遊ぶことは見送りました。
私が見つけたものは2つで、zag, zhp というものがありました。

zhp
https://github.com/frmdstryr/zhp

zap
https://github.com/zigzap/zap

また、Zigはこれといったパッケージマネージャーを持っていません。
個人で作成したものは割愛します。その点も見送りにした理由です。

Zig doesn’t have a package manager like rust does
https://news.ycombinator.com/item?id=28230680

Nimは「nimble」というパッケージ管理ツールを持っており、言語を導入した時点で使用できるため、Nimを選んだ理由の一つです。

Nimのフレームワークやライブラリ、開発ツールなどをまとめる「awesome Nim」のgithubのレポジトリを見るとわかりますが、分野を多岐に渡らせようとしているコミュニティの努力が伺えます。

同レポジトリの「frameworks」のタイトルのところでNim製のweb framework リストが確認できます。


その中でも興味が沸いたのが、「std/asynchttpserver」、「jester」、「prologue」です。
これらを選んだ共通する点は、比較的評価があり、ドキュメントや例が他よりも揃っているところです。

「std/asynchttpserver」 はNimが標準で持っている非同期のhttpサーバーモジュールです。
ドキュメントの充実は申し分ないのですが、フレームワークではないので、スクラッチで頑張る必要があり、ゆっくり時間があるときに試したいと思いました。

https://nim-lang.org/docs/asynchttpserver.html

次に「prologue」というものですが、ドキュメントなどにあった例は、 どこかexpress.jsに近い構文の印象を受けました。

公式GithubのREADMEに「TODO」アプリや、「ブログ」アプリのレポジトリのリンクがあり、 すぐに遊ぶには持ってこい感じでした。

最後に「jester」ですが、おそらく挙げたもの中で一番とっつき易くて、極めてシンプルにwebアプリが作成できそうです。

それもそのはずで、このフレームワークは軽量でシンプルな構文で有名なrubyの「sinatra」というに構文に寄せていて、ちょっとしたものを作りたい場合には大変便利な印象を受けました。

なので、この「jester」で簡単なwebアプリを作ってみようと思います。
その前に環境を用意する必要があるため、今回は環境を用意する方法を参考程度に記事にしました。

Nimの開発環境を準備

以外と簡単でした。というのもNimのコミュニティは公式でDocker環境を用意しているからです。

https://hub.docker.com/r/nimlang/nim/

私は手元のパソコンの環境汚したくないので、こちらで試すことにしました。

Docker環境を用意する

まずはフォルダ構成を用意します。

mkdir nim-docker-root # Dockerのディレクトリを作成
cd nim-docker-root # 作成したディレクトリに入る

# 作成したディレクトリ内で以下のようなファイル構造を用意する
├── docker # フォルダ
│   └── nim # フォルダ
│       ├── Dockerfile # nimのコンテナを定義するDockerファイル
├── docker-compose.yml # 環境変数、ポート指定等はこちらdocker-compose側で用意
└── nim_lang # nimの言語を試すnimファイルを基本こちら側に置く構成に
    ├── 01_main_hello # フォルダ
       └── hello.nim # 実際に試すnimファイル

次に、Dockerfileを定義します。一旦、言語の実行だけならば以下で十分です。

# Dockerfile

FROM nimlang/nim:latest

ENV APP_ROOT="/root/nim_lang"
WORKDIR $APP_ROOT

RUN apt-get update && apt-get install -y \
vim \
curl \
mingw-w64

WORKDIR $APP_ROOT
EXPOSE 5000

続いて、docker-compose.yml を定義します。

# docker-compose.yml

version: '3.8'

services:
app:
build:
context: ./
dockerfile: ./docker/nim/Dockerfile
volumes:
- ./nim_lang/:/root/nim_lang
ports:
- "50000:5000"
tty: true
privileged: true

次にイメージをビルドします。

cd nim-docker-root
docker-compose build

ビルドが終わったら、起動して、コンテナに入り、簡単なnimのスクリプトを実行しましょう。

コンテナを起動する

cd nim-docker-root
docker-compose up

コンテナに入る

docker-compose exec app bash -l  

dockerfile側で定義したWORKDIRのパスがコンテナログイン直後の現在のログインとなります。

/root/nim_lang/  

以下のファイルを作成してください。

root@1549d7124491:~/nim_lang # ls -las 01_main_hello/  
4 -rw-r--r-- 1 root root 46 Jul 3 02:12 hello.nim

hello.nimファイルに簡単な文字列を出力するスクリプトを書きます。

# nimで関数はpropcキーワードを使用して書きます。  
proc main() =
echo "HELLO FROM NIM"

# MAINもどき
main()

最後にnimのファイルを実行してみます。

cd /root/nim_lang/ 
nim c -r --verbosity:0 hello.nim
HELLO FROM NIM

このように簡単に環境の用意も、スクリプトの実行もすることができました。

次に簡単なweb apiを作ってみましょう。

簡単なapiを用意する

上で用意したDocker関連のファイルをapi用にちょっと作り直します。

まずはフォルダ構成ですが、以下のようにご準備ください。

├── docker
│   └── nim
│       ├── Dockerfile
│       └── scripts # これから作るサーバースクリプトをshellから実行したくこちらも用意してください
│           └── run-server.sh # 空でOK
├── docker-compose.yml
├── nim_api # フォルダ新規作成(空でOK)

先程用意したnim_langフォルダはもう不要となるので削除してください。
かわりに、npm_apiというフォルダを用意します。

コンテナログイン後のディレクトリをnim_apiにしたいので、Dockerfileを調整します。

FROM nimlang/nim:latest

ENV APP_ROOT="/root/nim_api" # ここをnim_lang から nim_api に変更しました。
WORKDIR $APP_ROOT

RUN apt-get update && apt-get install -y \
vim \
curl \
mingw-w64

WORKDIR $APP_ROOT

EXPOSE 5000

Nimのサーバースクリプトを実行するためのシェルを用意します。

ファイルはすでに作成されている前提のため、あとは以下のようなスクリプトを書きます。

# run-server.sh

#!/bin/bash
set -e

# 以下サーバーを起動するスクリプト
echo "run nim server..."
cd /root/nim_api && \
nim c -r src/nim_api.nim

exec "$@"

docker-compose.ymlは以下のように調整します。

version: '3.8'

x-api-vars: &api-vars
COMPOSE_PROJECT_NAME: nim_api_development
NIM_ENV: development

services:
app:
build:
context: ./
dockerfile: ./docker/nim/Dockerfile
command: ["bash", "-c", "/root/scripts/run-server.sh"] # コンテナ起動時上で定義したシェルを実行
volumes:
- ./nim_api/:/root/nim_api # APIフォルダをマウント
- ./docker/nim/scripts/:/root/scripts # サーバー起動スクリプトをマウント
environment: *api-vars # 環境変数の読込み
ports:
- "50000:5000" # バインドするポート番号(コンテナのそとからのアクセスは50000ポートで指定)
tty: true
privileged: true

最後にビルドを再び行いましょう。

 cd nim-docker-root
docker-compose build

ビルドが終わりましたら、アドホックで起動してコンテナにログインしましょう。

docker-compose run --rm app bash -l

ログイン直後は、/root/nim_api/ になったことを確認できたかと思います。

一度コンテナを抜けて、引き続きフォルダ構成を調整しましょう。

nim_apiフォルダは空ですので、ここでサーバープロジェクトを作成したいと思います。

まずは nimble init コマンドで新規プロジェクトを作成します

cd /root/nim_api/
nimble init -Y

すると、以下のようはフォルダ構成が作成されました。

root@5ed3f0002b01:~/nim_api# ls
nim_api.nimble
src
tests

作成されたフォルダの中身を確認します。


root@1549d7124491:~/nim_api# ls -las
nim_api.nimble # 今回はここに必要なライブラリを定義します。
src
tests

# src/の中身
root@1549d7124491:~/nim_api# ls -las src/
nim_api
nim_api.nim

今回、テストは割愛してsrc配下でサーバースクリプトを用意します。
src/配下に作られるnim_api.nimファイルの内容は使用しないため、空にしてください。

JSONファイルをDBもどきに使用して、サーバースクリプトで読込み、
その内容をレスポンスで返すような簡単なapiを作成します。

まずは、JSONファイルを用意しましょう。

cd /root/nim_api/
mkdir data
cd data/
touch db.json

コンテナの中でjsonファイルが作成できましたので、次にデータを用意します。

# ユーザーレコードが2つあるような構成です
[
{
"id": 1,
"name": "john"
},
{
"id": 2,
"name": "mike"
}
]

次に、上記で少し触れた「nim_api.nimble」ファイルに、今回使用するパッケージを追加します。

「nim_api.nimble」はNimのパッケージ管理ツールのファイルで、こちらにプロジェクトで使用したいライブラリを追加してインストールすることで使用できるようになります。

rubyでいうとgemfileと同じようなものです。

では、以下のように追加していきましょう。

今回はjesterというwebフレームワークを使用するので以下のように追加しました。

# Package
version = "0.1.0"
author = "Anonymous"
description = "A new awesome nimble package"
license = "MIT"
srcDir = "src"

# Dependencies
requires "nim >= 1.6.12"
requires "jester" # ★この一行が追加したものです。

上ので定義が済んだら、インストールを行いましょう。

cd /root/nim_api/
nimble install -Y

以下が成功時のログです。

  Verifying dependencies for nim_api@0.1.0
Prompt: No local packages.json found, download it from internet? -> [forced yes]
Downloading Official package list
Success Package list downloaded.
Installing jester@any version
Downloading https://github.com/dom96/jester using git
Verifying dependencies for jester@0.6.0
Installing httpbeast@>= 0.4.0
Downloading https://github.com/dom96/httpbeast using git
Verifying dependencies for httpbeast@0.4.1
Installing asynctools@#0e6bdc3ed5bae8c7cc9
Downloading https://github.com/cheatfate/asynctools using git
Verifying dependencies for asynctools@#0e6bdc3ed5bae8c7cc9
Installing asynctools@#0e6bdc3ed5bae8c7cc9
Success: asynctools installed successfully.
Installing httpbeast@0.4.1
Success: httpbeast installed successfully.
Installing jester@0.6.0
Success: jester installed successfully.
Installing nim_api@0.1.0
Success: nim_api installed successfully.

これで、jesterのライブラリの導入は終わりです。


次に今回メインのサーバースクリプトを用意します。

先程空にした「nim_api.nim」ファイルに以下のようなapiのスクリプトを書きます。

import json
import jester
import asyncdispatch
import os,strutils,tables
import algorithm,hashes,math,sequtils
import std/htmlgen

# ルート定義
routes:
  # curl -X GET "http://0.0.0.0:50000/api"
  get "/api":
    # jsonで定義したユーザーの配列情報を一覧としてレスポンスに返すシンプルなAPI
    let dataStr = parseFile("/root/nim_api/src/data/db.json").pretty
    let data = parseJson(dataStr)
    let response = %*{"data": data}
    resp $response, "application/json"

  # curl -X GET "http://0.0.0.0:50000/api/1"
  get "/api/@id":
    # jsonで定義したユーザーの配列情報からパラメータで受けたユーザーIDを使用して単一のユーザー情報を
    # レスポンスに返すシンプルなAPI
    echo @"id"
    let id = parseInt(@"id")
    let dataStr = parseFile("/root/nim_api/src/data/db.json").pretty
    let data = parseJson(dataStr)

    for item in data:
      if item["id"].getInt() == id:
        let response = %*{"data": item}
        resp $response, "application/json"
        break

    let noResponse = %*{"data": nil}
    resp $noResponse, "application/json"

  # curl -X GET "http://0.0.0.0:50000/html/mike"
  get "/html/@name":
    # std/htmlgenという標準のHTML構築ライブラリを使用して、動的なwebページを返すAPI
    let paragraph = "hello " & @"name"
    let h1 = h1("HELLO WEB PAGE")
    let a = a(href = "https://nim-lang.org/", "nim lang")
    let p = p(paragraph)

    let html = h1 & a & p
    resp(Http200, html)

# サーバー起動
runForever()

これで、サーバーのスクリプトの用意は完了しました。

次にコンテナから一度抜けます。

exit

サーバー起動は先程用意したシェルスクリプトに任せます。

cd nim-docker-root
docker-compose up

以下のような出力が確認できればサーバーも起動中です。

app_1  | Hint:  [Link]
app_1  | Hint: gc: refc; opt: none (DEBUG BUILD, `-d:release` generates faster code)
app_1  | 88800 lines; 4.652s; 144.598MiB peakmem; proj: /root/nim_api/src/nim_api.nim; out: /root/nim_api/src/nim_api.out [SuccessX]
app_1  | Hint: /root/nim_api/src/nim_api.out  [Exec]
app_1  | INFO Jester is making jokes at http://0.0.0.0:5000
app_1  | Starting 1 threads
app_1  | Listening on port 5000

内部的には5000番ポート(jester標準のポート番号)ですが、コンテナの外からapiにリクエストをするときは、50000で指定した点だけ注意してください。

APIを試す

ユーザーの一覧を返すAPIの結果

12:31:38 nim-docker-root λ > curl -X GET "http://0.0.0.0:50000/api" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    56  100    56    0     0   6552      0 --:--:-- --:--:-- --:--:--  9333
{
  "data": [
    {
      "id": 1,
      "name": "john"
    },
    {
      "id": 2,
      "name": "mike"
    }
  ]
}


ユーザーID指定でユーザーの単一情報を返すAPIの結果

12:33:01 nim-docker-root λ > curl -X GET "http://0.0.0.0:50000/api/1" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    31  100    31    0     0   3384      0 --:--:-- --:--:-- --:--:--  5166
{
  "data": {
    "id": 1,
    "name": "john"
  }
}

最後のユーザー名をパラメータとして渡して、webページを返してもらうAPIですが、
curlでAPIリクエストをしてもHTML情報しか返さないので、こちらブラウザからアクセスして、
結果を確認しましょう。

以下のように「mike」というユーザー名のパラメータを指定してブラウザアクセスしてみます。

http://0.0.0.0:50000/html/mike

するとこの用にwebページが返却されたのを確認できます。

まとめ

以上で、Nimで遊べる簡単な環境構築を紹介しました。
Nimはwebフレームワーク以外に、組込、CLI、ゲーム、GUIのライブラリもあるので、
今後はそういったものも試したいと思います。
今回の記事がNimの環境構築にお役に立てれば幸いです。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる