はじめに
こんにちは、高校2年の樅山です。
この記事は、ブログ HookRace Blog で公開されている「What is special about Nim?」(2015年1月1日)を、著者 Dennis Felsing 氏の許可を得て翻訳し、投稿したものです。
この記事は、「Nim Advent Calendar 2020 その2」の5日目です。
Nimのどこが特別なのか?
Nimは、私たちをワクワクさせるプログラミング言語です。
公式サイトには高品質なチュートリアルがありますが、段階を踏んで詳細にNimを紹介するものです。
かわりにこの記事では、他のプログラミング言語では不可能なことや、難しいけれども実装できることについて手っ取り早く紹介したいと思います。
私は、現在のDDNetのゲームMODである、Teeworldsを実装するのに適切な言語を探していたときにNimと出会いました。
今は他のプロジェクトで忙しいので、このブログ、HookRaceはNimについてのブログになりました。
簡単に実行できる
まだワクワクできる内容ではありませんが、記事に沿って進めていくことをおすすめします!
for i in 0..10:
echo "Hello World"[0..i]
もし上のコードを動かしたいなら、Nimコンパイラをインストールしてください。
ソースコードをコピーして、hello.nim
として保存し、nim c hello
を実行してコンパイルして、最後に./hello
でバイナリを実行します。
デバッグビルドの代わりに、最適化されたリリースビルドを使用するにはnim -d:release c hello
を実行します。これら全ての設定で、次の出力が表示されます。
H
He
Hel
Hell
Hello
Hello
Hello W
Hello Wo
Hello Wor
Hello Worl
Hello World
コンパイル時にコードを実行する
効率的なCRC32プロシージャを実装するには、ルックアップテーブルが必要です。
これは実行時に計算可能で、魔法の配列としてコードに書き込むこともできます。明らかにマジックナンバーは必要ないので、今のところは実行時に計算します。
import unsigned, strutils
type CRC32* = uint32
const initCRC32* = CRC32(-1)
proc createCRCTable(): array[256, CRC32] =
for i in 0..255:
var rem = CRC32(i)
for j in 0..7:
if (rem and 1) > 0: rem = (rem shr 1) xor CRC32(0xedb88320)
else: rem = rem shr 1
result[i] = rem
# 実行時にテーブルを作成する
var crc32table = createCRCTable()
proc crc32(s): CRC32 =
result = initCRC32
for c in s:
result = (result shr 8) xor crc32table[(result and 0xff) xor ord(c)]
result = not result
# echoプロシージャによって、文字列に型変換する `$`プロシージャが暗黙に呼び出される
proc `$`(c: CRC32): string = int64(c).toHex(8)
echo crc32("The quick brown fox jumps over the lazy dog")
いいですね!これで414FA339
が出力されました。
しかし、コンパイル時にCRCテーブルを計算できればさらに良いでしょう。
これは、現在使用しているcrc32
テーブルの作成の代わりにNimで記述するのと同じくらい簡単です。
# コンパイル時に作成されるテーブル
const crc32table = createCRCTable()
私たちがするべきことは、var
をconst
に置き換えるだけです。素晴らしい!全く同じコードにもかかわらず、実行時実行とコンパイル時実行を切り替えることができます。テンプレートメタプログラミングは必要ありません。
言語を拡張する
テンプレートとマクロを使用すると、コンパイル時にコードを変換することでボイラープレートを除去できます。
テンプレートを用いてコンパイル時にコードを置換することで、独自のループを定義する例を示します。
訳注
HookRaceで公開されたコードが執筆当時のバージョン(1.4.4)では動作しないため修正しています。具体的には、expr
型・stmt
型は現在利用不可能で、具体型やuntyped
型を用います。
template times(x: int, y: untyped): untyped =
for i in 1..x:
y
10.times:
echo "Hello World"
times
テンプレートは次のようなプログラムに展開されます。
for i in 1..10:
echo "Hello World"
10.times:
という構文は、10
をtimes
の1番目のパラメータとして与え、2番目のパラメータとしてインデント内のブロックを渡しています。
詳しくは、統一呼び出し記法で説明します。
動的配列をもっと楽に初期化することもできます。
template newSeqWith(len: int, init: untyped): untyped =
var result = newSeq[type(init)](len)
for i in 0..<len:
result[i] = init
result
# 大きさ 20x10 の二次元動的配列を作成する
var seq2D = newSeqWith(20, newSeq[bool](10))
import math, random
randomize()
# 20個の10以下の乱数列を生成する
var seqRand = newSeqWith(20, rand(10))
echo seqRand
マクロはさらに一歩進んで、抽象構文木(AST)を分析・操作できます。
たとえばNimはリスト内包表記を持ちませんが、マクロを使って実装できます。
var res: seq[int] = @[]
for x in 1..10:
if x mod 2 == 0:
res.add(x)
echo res
const n = 20
var result: seq[tuple[a,b,c: int]] = @[]
for x in 1..n:
for y in x..n:
for z in y..n:
if x*x + y*y == z*z:
result.add((x,y,z))
echo result
future
モジュールを使えば、次のように書き直すことができます
訳注
現在のバージョン(1.4.4)では、future
モジュールからlc
マクロは削除されました。
過去の実装を参考にしながらlc
マクロを実装することは、マクロを習得する上で良い練習になるでしょう。
import future
echo lc[x | (x <- 1..10, x mod 2 == 0), int]
const n = 20
echo lc[(x,y,z) | (x <- 1..n, y <- x..n, z <- y..n, x*x + y*y == z*z), tuple[a,b,c: int]]
コンパイラに独自の最適化を追加する
コードを最適化するのではなく、賢いコンパイラにしたいと思いませんか?Nimではそれができます。
var x: int
for i in 1..1_000_000_000:
x += 2 * i
echo x
この(だいぶ役に立たない)コードは、コンパイラに2つの最適化を教えることでスピードを上げることができます。
template optMul{`*`(a,2)}(a: int): int =
let x = a
x + x
template canonMul{`*`(a,b)}(a: int{lit}, b: int): int =
b * a
最初の項書き換えテンプレートでは、a * 2
をa + a
に置換可能であることを指定します。
2つ目のテンプレートでは、1つ目のパラメータが整数リテラルであれば乗算の整数を入れ替えることができることを指定します。
より複雑なパターンを実装することも可能で、例えば真偽値ロジックを最適化できます。
template optLog1{a and a}(a): auto = a
template optLog2{a and (b or (not b))}(a,b): auto = a
template optLog3{a and not a}(a: int): auto = 0
var
x = 12
s = x and x
# optLog1(x) --> ’x’
r = (x and x) and ((s or s) or (not (s or s)))
# optLog2(x and x, s or s) --> ’x and x’
# optLog1(x) --> ’x’
q = (s and not x) and not (s and not x)
# optLog3(s and not x) --> ’0’
optLog1
最適化によってs
は直接x
に変換されます。
optLog2
最適化によってr
はx and x
に変換された後、optLog1
最適化によってx
に変換されます。
optLog3
最適化によってq
は0
に変換されます。
お気に入りのC言語の関数やライブラリにバインドする
NimはC言語にコンパイルされるので、外部関数インターフェース(FFI)もご愛敬。
C言語のライブラリから気に入っている関数を簡単に使用できます。
proc printf(formatstr: cstring)
{.header: "<stdio.h>", varargs.}
printf("%s %d\n", "foo", 5)
または、自分で記述したCプログラムを呼び出すこともできます。
void hi(char* name) {
printf("awesome %s\n", name);
}
{.compile: "hi.c".}
proc hi*(name: cstring) {.importc.}
hi "from Nim"
c2nim
を使えば、好きなライブラリを呼び出せます。
proc set_default_dpi*(dpi: cdouble) {.cdecl,
importc: "rsvg_set_default_dpi",
dynlib: "librsvg-2.so".}
ガベージコレクタを制御する
ソフトリアルタイムを実現するには、ガベージコレクタがいつ、どのくらいの時間実行できるかを伝えなければいけません。
メインのゲームロジックは、ガベージコレクタがスタッタを起こすのを防ぐために、Nim にこのように実装することができます。
gcDisable()
while true:
gameLogic()
renderFrame()
gcStep(us = leftTime)
sleep(restTime)
型安全な集合と列挙型の配列
しばしば、自分で定義した値の上に数学的な集合が必要になることがあります。これを型安全に行う手法を紹介します。
type FakeTune = enum
freeze, solo, noJump, noColl, noHook, jetpack
var x: set[FakeTune]
x.incl freeze
x.incl solo
x.excl solo
echo x + {noColl, noHook}
if freeze in x:
echo "Here be freeze"
var y = {solo, noHook}
y.incl 0 # Error: type mismatch
誤って別の型の値を追加することはできません。
集合は内部で効率的なビットベクターとして動作します。
同じことが配列でも可能で、インデックスに列挙を利用します。
var a: array[FakeTune, int]
a[freeze] = 100
echo a[freeze]
統一呼び出し記法
これはただの糖衣構文ですが、確かにあった方がいいですね。
Pythonではlen
やappend
が関数かメソッドかをいつも忘れてしまいます。しかし、Nimではその違いがないので覚える必要はありません。
Nimでは統一呼び出し記法を用いていますが、C++でもHerb Sutter氏とBjarne Stroustrup氏によって提案されています。
var xs = @[1,2,3]
# プロシージャ呼び出し構文
add(xs, 4_000_000)
echo len(xs)
# メソッド呼び出し構文
xs.add(0b0101_0000_0000)
echo xs.len()
# コマンド呼び出し構文
xs.add 0x06_FF_FF_FF
echo xs.len
良好な性能
Longest Path Finding BenchmarkにあるNimのコードを見てもわかるように、高速なコードを書くのは簡単です。
ベンチマークが公開された当初のマシン(Linux x86-64, Intel Core2Quad Q9300 @2.5GHz, state of 2014-12-20)でいくつか計測してみました。
言語 | 時間[ms] | メモリ[KB] | コンパイル時間[ms] | 圧縮されたコード[B] |
---|---|---|---|---|
Nim | 1400 | 1460 | 893 | 486 |
C++ | 1478 | 2717 | 774 | 728 |
D | 1518 | 2388 | 1614 | 669 |
Rust | 1623 | 2632 | 6735 | 934 |
Java | 1874 | 24428 | 812 | 778 |
OCaml | 2384 | 4496 | 125 | 782 |
Go | 3116 | 1664 | 596 | 618 |
Haskell | 3329 | 5268 | 3002 | 1091 |
LuaJIT | 3857 | 2368 | - | 519 |
Lisp | 8219 | 15876 | 1043 | 1007 |
Racket | 8503 | 130284 | 24793 | 741 |
gzip -9 < nim.nim | wc -c
でコードサイズを圧縮します。
Haskellで使われていない関数を削除しました。
Nimの場合、コンパイル時間はクリーンなコンパイルで標準ライブラリをプリコンパイルしたnimcacheがあれば、323msで済みます。
もう一つは、Python、Nim、Cで、1億回素数かどうかを計算するベンチマークです。
Python (実行時間: 35.1s)
def eratosthenes(n):
sieve = [1] * 2 + [0] * (n - 1)
for i in range(int(n**0.5)):
if not sieve[i]:
for j in range(i*i, n+1, i):
sieve[j] = 1
return sieve
eratosthenes(100000000)
Nim (実行時間: 2.6s)
import math
proc eratosthenes(n): auto =
result = newSeq[int8](n+1)
result[0] = 1; result[1] = 1
for i in 0 .. int sqrt(float n):
if result[i] == 0:
for j in countup(i*i, n, i):
result[j] = 1
discard eratosthenes(100_000_000)
C (実行時間: 2.6s)
#include <stdlib.h>
#include <math.h>
char* eratosthenes(int n)
{
char* sieve = calloc(n+1,sizeof(char));
sieve[0] = 1; sieve[1] = 1;
int m = (int) sqrt((double) n);
for(int i = 0; i <= m; i++) {
if(!sieve[i]) {
for (int j = i*i; j <= n; j += i)
sieve[j] = 1;
}
}
return sieve;
}
int main() {
eratosthenes(100000000);
}
JavaScriptにコンパイルする
C言語の代わりにJavaScriptにトランスパイルできます。
これにより、サーバーだけでなくWebフロントエンドもNimで直接書くことができます。ブラウザに表示されるサーバー上の訪問者カウンターを作ってみましょう。
import htmlgen, dom
type Data = object
visitors {.importc.}: int
uniques {.importc.}: int
ip {.importc.}: cstring
proc printInfo(data: Data) {.exportc.} =
var infoDiv = document.getElementById("info")
infoDiv.innerHTML = p("You're visitor number ", $data.visitors,
", unique visitor number ", $data.uniques,
" today. Your IP is ", $data.ip, ".")
サーバーからクライアントへのデータの受け渡しに使用するデータ型を定義します。
printInfo
プロシージャが呼び出され、表示されます。
これを、nim js client
でコンパイルします。
結果のJavaScriptファイルはnimcache/client.jsで終了します。
サーバーサイドでは、Nibleパッケージマネージャを取得します。
nimble install jester
を実行してJesterというWebフレームワークを使って書くことができます。
import jester, asyncdispatch, json, strutils, times, sets, htmlgen, strtabs, httpcore
var
visitors = 0
uniques = initSet[string]()
time: TimeInfo
routes:
get "/":
resp body(
`div`(id="info"),
script(src="/client.js", `type`="text/javascript"),
script(src="/visitors", `type`="text/javascript"))
get "/client.js":
const result = staticExec "nim -d:release js client"
const clientJS = staticRead "nimcache/client.js"
resp clientJS
get "/visitors":
let newTime = getTime().getLocalTime
if newTime.monthDay != time.monthDay:
visitors = 0
init uniques
time = newTime
inc visitors
let ip =
if request.headers.hasKey "X-Forwarded-For":
request.headers["X-Forwarded-For", 0]
else:
request.ip
uniques.incl ip
let json = %{"visitors": %visitors,
"uniques": %uniques.len,
"ip": %ip}
resp "printInfo($#)".format(json)
runForever()
サーバーサイドでは、Webサイトを配信します。
また、コンパイル時にclient.nimを読み込んでコンパイルすることで、client.jsを配信します。
ロジックは、/visitors
の処理にあります。
コンパイルして、nim -r c server
で実行し、http://localhost:5000/ を開いてみましょう。
Jesterで生成されたサイトやインラインで動作中のコードを確認できます。
あなたは今日の2人目の訪問者です。
あなたのIPアドレスは126.72.114.43です。
終わりに
Nim言語に興味を持っていただければ幸いです。
この言語はまだ完全に安定していないことに注意してください。特に、より曖昧な機能ではバグを踏み抜くかもしれません。しかし、Nim ver.1.0は3ヶ月以内にリリースされると言われています。だから、今がNimを始めるのに最適です。
訳注
2019年9月23日にNimのver.1.0が公開されました。
詳しくは公式ブログをご覧ください。
NimはC言語にコンパイルされ、Cの標準ライブラリのみに依存しているので、x86-64、ARM、Intel Xeon Phi accelerator cardsなど、ほとんどどこにでもデプロイできます。
この記事へのコメントは、RadditやHacker Newsを使うか、IRCで直接Nimコミュニティに質問してください(#nim on freenode)。
著者に直接連絡を取りたい場合は dennis@felsin9.de にメールしてください。
この記事の校正を担当してくれたAndreas Rumpf氏とDominik Picheta氏に感謝します。
訳者より: 終わりに
Dennis Felsing氏に快く許可をいただけたため、本記事を執筆することができました。ありがとうございました。
この記事は2015年に執筆されました。つまり6年前の記事ですが、日本では詳細なドキュメントがまだまだ不足しているので新鮮に映る機能もあったはずです。私は、項書き換えテンプレートによる最適化がわかりやすく説明されていたことに感動しました。
しかし、Nim ver.1.0は3ヶ月以内にリリースされると言われています。
これは少し面白いですね。実際は4年間かかっています。
ただ、Nimは更新頻度が遅いわけではなく大量のバグが修正され、新しい実験的機能も追加されています。
Nimは本当に面白い言語です。
文法の平易さや速度に留まらず、モダンな機能や面白い機能をたくさん利用することができます。素晴らしい拡張性によって、サードパーティ製の機能も充実しています。
ぜひ、Nimを始めてみませんか?