LoginSignup
39

More than 1 year has passed since last update.

posted at

updated at

Organization

Nimのどこが特別なのか?【翻訳】

はじめに

こんにちは、高校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()

私たちがするべきことは、varconstに置き換えるだけです。素晴らしい!全く同じコードにもかかわらず、実行時実行とコンパイル時実行を切り替えることができます。テンプレートメタプログラミングは必要ありません。

言語を拡張する

テンプレートマクロを使用すると、コンパイル時にコードを変換することでボイラープレートを除去できます。

テンプレートを用いてコンパイル時にコードを置換することで、独自のループを定義する例を示します。

訳注
HookRaceで公開されたコードが執筆当時のバージョン(1.4.4)では動作しないため修正しています。具体的には、expr型・stmt型は現在利用不可能で、具体型やuntyped型を用います。

timesテンプレートを定義する
template times(x: int, y: untyped): untyped =
  for i in 1..x:
    y

10.times:
  echo "Hello World"

timesテンプレートは次のようなプログラムに展開されます。

展開されたtimesテンプレート
for i in 1..10:
  echo "Hello World"

10.times:という構文は、10timesの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 * 2a + 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最適化によってrx and xに変換された後、optLog1最適化によってxに変換されます。
optLog3最適化によってq0に変換されます。

お気に入りの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ではlenappendが関数かメソッドかをいつも忘れてしまいます。しかし、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で直接書くことができます。ブラウザに表示されるサーバー上の訪問者カウンターを作ってみましょう。

client.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フレームワークを使って書くことができます。

server.nim
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を始めてみませんか?

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
What you can do with signing up
39