読者です 読者をやめる 読者になる 読者になる

Nim帳

プログラミング言語Nimについて書いていきます。

20160921155215

Nimのおすすめ文献紹介

Nimのおすすめ文献の紹介です。日本語情報が少ないため英語のものが多めになります。

What is special about Nim? (Nimの特別なところは何か)

URL: https://hookrace.net/blog/what-is-special-about-nim (英語)

Nimのいいところを挙げた記事です。ひたすらにNimを褒めまくるという記事になっています。
Nim1.0が3か月以内にリリースされると書いてありますが、まだ0.15.2なのは注意です。(はやく1.0出てほしい・・・)

SDL2を使い、Nimで2Dのプラットフォーム・ゲームを書く

URL: http://postd.cc/writing-a-2d-platform-game-in-nim-with-sdl2/ (英語から日本語への翻訳)
原文: https://hookrace.net/blog/writing-a-2d-platform-game-in-nim-with-sdl2/

上記の What is special about Nim? と同じ方が書かれた記事です。こちらはPOSTDさんが日本語に翻訳されているようです。

SDLという最小限のメディアライブラリを使ってゲームを作るという記事です。
Nimでの基本的な書き方から発展に加え、配布向けのバイナリをビルドするという実践的な話題も扱っている網羅的な記事なので特におすすめです。普通のゲームプログラミング入門としての出来も素晴らしいのもいいですね。

Nim by Example (OOP Macro)

URL: https://nim-by-example.github.io/oop_macro/ (英語)

Nimのマクロを使い、C++風のclass構文を実現するという黒魔術面白い記事になっています。Nimのマクロの入門としてもおすすめです。

Go-lang like interface

URL: http://forum.nim-lang.org/t/2422#14994 (英語)

こちらはフォーラムのポストになります。内容は、Nimにはinterface無いけどマクロ使ってGo言語みたいなinterface作ったよ!的な記事です。
Nimは標準で言語機能が充実しているので実際に利用する機会は少ないかもしれませんが、Nimの強力さが分かりますね。

Nim binary size from 160 KB to 150 Bytes (Nimのバイナリサイズを160KBから150Bにする)

(追記分)
URL: https://hookrace.net/blog/nim-binary-size/(英語)

NimのバイナリサイズをLinux上で最小限にするという記事です。

まとめ

以上が現在おすすめの記事です。他にも面白い記事を発見したら追記したいと思います。

Nimの導入

以前のブログに描いたNimの記事がNim 入門で検索するとトップにくるという想定外の事態になり、さらにNimのバージョンも進み記事の内容が古くなってしまったので、これはきちんと書き直さないといけないと思い、こちらのブログで導入記事を書き直すことにしました。

この記事はNimのバージョン0.15.2時点での記事です。おそらくしばらく経てばまた古くなると思います。その時はまた記事を更新しようと思います。

この記事ではWindowsを想定しています。Linux向けの記事はまたいずれ書きたいと思います。

必要なもの

Nimはコンパイルの際、基本的にCを通してバイナリにコンパイルするので、必然的にCのコンパイラが必要になります。
Nimで使うCコンパイラは様々なものがサポートされており、下記のものが公式にサポートされているようです。

他のものが使える場合もあるそうですが、おそらく非推奨になるでしょう。
ここでは一番無難なGCCを導入します。

ダウンロード

Nimコンパイラ

Windowsの場合、Nim公式のdownloadページにあるZipsの32bitか64bitのどちらかを好みで選びましょう。
ここで重要なのが後述のCコンパイラとbitを合わせることです。32bitのNimコンパイラと64bitのCコンパイラを組み合わせると動かない場合があるので、合わせてダウンロードしましょう。
ここでは64bit版を導入します。

Zipsの下にはExesがありますが、 これはインストーラー形式でインストールができ、 同時にCコンパイラやAporia IDE(Nim用IDE)などもインストールできる優れものだったのですが、 0.15.0でバグが見つかり、メンテナンスも困難になっているようで、現在使うのは非推奨になっています。 とても便利なものだったので是非復活してほしいですね。

Cコンパイラ

GCCWindowsの場合、おそらくTDM-GCCがいいと思います。 GCCは他にもmingw-w64からもダウンロードでき、Nim公式はこちらを推奨していますが、どれをダウンロードすればいいかわかりにくいのと、Msys2やCygwinなど今回必要なCコンパイラに加えて他の物もダウンロードしてきてしまうものが多いので、導入がしやすいTDM-GCCにしました。

TDM-GCCのDownloadページに行き、tdm64-gcc-[バージョン].exeをダウンロードします。

インストール

Nimコンパイラはダウンロードしたzipファイルを任意のディレクトリに展開し、展開したディレクトリのbinディレクトリにPATHを通しましょう。
TDM-GCCはインストールの際に出てくる選択でMinGW-w64/TDM64を選んでインストールしましょう。ここではデフォルトのディレクトリ(C:/TDM-GCC-64)にインストールしたという前提でいきます。

設定

最後に設定です。このままではNimコンパイラにTDM-GCCの情報が伝わってないので、設定ファイルに追記します。
展開したNimコンパイラディレクトリ(ここではnim-0.15.2)のconfigディレクトリにあるnim.cfg

@if windows:
  gcc.path = r"C:\TDM-GCC-64\bin"
@end

を追記します。
TDM-GCCのインストール場所を変えた場合は各自gcc.pathの変更をお願いします。
これでインストールは完了です。

使い方

インストールが上手くいったかの確認として、試しにコンパイルしてみます。 下記のコードをhelloworld.nimというファイルに保存し、

# helloworld.nim
echo "Hello World!!"

下記のコマンドを実行でハローワールドプログラムのコンパイルです。

nim c helloworld.nim 

あとは

helloworld.exe

Hello World!!が表示されれば確認完了です。

まとめ

バージョン0.15.2現在では結構インストールが面倒くさくなってしまっています。おそらく将来的(1.0が出るまで)には改善されると思うので期待して待ちましょう。

補足

Nimは0.15からwindowsでの端末出力がUTF-8になり、Windowsコマンドプロンプトでは日本語を使った場合文字化けするようになってしまいました。
一時的な対策として、minttyをUTF-8で使うなどするしかないと思います。
おすすめはGitをインストールした際に付いてくるGit Bashをminttyで使うあたりでしょうか。
こちらもいずれ改善してほしいですね。

0.15.0ではUTF-8になってましたが、0.15.2からはもとに戻ったようです。 現在調査中です。

Nimのモジュール

Nimは普段のプログラミングに使える汎用のプログラミング言語ですが、 言語としてはシステムプログラミングを意識している言語です。
システムプログラミング言語としてはC/C++がメジャーな言語ですが、NimではC/C++と違って優秀なモジュールシステムを使うことができます。
今回はそんなモジュールの話です。

基本

基本的にはpython風の文法になっています。

import

Nimのモジュールシステムで他のモジュールを使うようにするには、

import モジュール名 

と書くことによって使えます。
例えば、標準ライブラリでよく使うstrutilsをimportするには

import strutils

とすればすぐに使うことができます。

except

importする際に一部の関数などをインポートしないようにするexceptがあります。

import strutils except `%`, toUpper

echo "$1" % "abc".toUpper # compile error!

from

他にも、特定の関数のみをインポートするfrom文があります。

from strutils import `%`
echo "$1" % "abc"
echo strutils.replace("abc", "a", "z") # モジュール名を含む場合使用可能

importはカンマで区切ることによって1行で複数モジュールをインポートすることができます。

import strutils, sequtils

さらに、asで別名にすることができます。

import strutils as su

インポートした関数などは、そのまま関数名で呼び出すことができ、strutils.formatのように呼び出すこともできます。
別のモジュール同士に同じ関数名が含まれている場合は、モジュール名をつけることによって区別することができます。

# modulea.nim
proc echoInt*(a: int) =
  echo a
# moduleb.nim
proc echoInt*(a: int) =
  echo a
import modulea
import moduleb

echoInt(1) # compile error!
modulea.echoInt(1) # works!
moduleb.echoInt(1) # works!

Nimのモジュールシステムではimport モジュール名とした際に、

  • 標準ライブラリ
  • パッケージマネージャのnimbleによってインストールされたライブラリ
  • ディレクト

からインポートしようとします。

インポートする際にはモジュール名に相対パスを使うことができ、相対パス/.に置き換えることもできます。

# modules/foo.nim
proc echoInt*(a: int) =
  echo a
import modules.foo

echoInt(1)

Asterisk(public)

サンプルの関数に付いているアスタリスク*は関数を公開(public)するようにする文法です。
関数以外にもマクロや型などにも付けることができます。

export

他モジュールの関数などを別モジュールから公開するexportがあります。

# objmodule.nim
type MyObject* = object
# basemodule.nim
import objmodule
export objmodule.MyObject

proc `$`*(x: MyObject): string = "my object"
# main.nim
import basemodule

var x: MyObject
echo $x

include

includeは他のファイルをそのまま取り込む文です。Cのプリプロセッサでいう#include <***.h>ですね。

include fileA, fileB, fileC

使いどころとしては複数のAPIのwrapperを作るとき、コンパイル時に使用するAPIを切り替えできるようにする際にwhenと組み合わせて使ったりします。

const audioAPI = "wasapi"
when audioAPI == "WASAPI":
  include wasapi.nim
elif audioAPI == "OpenAL":
  include openal.nim
elif audioAPI == "CoreAudio":
  include coreaudio.nim
else:
  raise newException(Exception, "Unknown Audio API")

nimble

nimbleはNimのパッケージマネージャ兼ビルドシステムです。
C/C++と違い、公式で用意されているものなのでほとんどのNimライブラリがコマンド一発で入るというとても便利なものになっています。

$ nimble install csv

上記のコマンドでcsvパーサが一発で入ります。
後は、

import csv

を書いて直ぐに使い始めることができます。

nimbleで入れられるライブラリとして特徴的なのはcompilerライブラリです。 compilerライブラリは文字通りコンパイラのライブラリで、これはNimのコンパイラのライブラリになっています。
NimはコンパイラがNim自身で書かれているので(セルフホスティング)、こうしてコンパイラ自身をライブラリとして使えるようになっています。

まとめ

Nimのモジュールシステムは非常に優れており、これだけでC/C++に比べてアドバンテージがあるのではないでしょうか。
特にcompilerライブラリは他の言語ではなかなか無いライブラリで、とても興味深いのでいずれ単体で記事にしたいところです。

おまけ

NimではC/C++のようにコンパイル時にコンパイラに必要なファイル全てを渡す必要はなく、一つのファイルを渡せば後はimportから自動的に解析してコンパイルしてくれます。
さらにNimではインクリメンタルコンパイルができ、一度コンパイルしてしまえばあとは更新があった部分のみを再コンパイルしてくれるので2回目以降のコンパイルは非常に高速です。
1回目のコンパイルは標準ライブラリを含めてコンパイルされるので時間がかかりますが、2回目以降は高速化されるので実用上問題になることは少ないでしょう。
Nimではロードマップでインクリメンタルコンパイルの大幅な改善も予定されているのでさらに高速になることが予想されるので期待ですね!

Nimの関数について

Nimでは関数のオーバーロードができます。Nimでは独自の演算子が定義でき、オーバーロードができるので、演算子を活用したライブラリなども作れます。(例:パーサコンビネータなど)
それだけでなく、method call syntaxもあることによって、独自のライブラリでもNimらしい見た目で使うことができるので、とても読みやすいソースコードになります。(Nimは速度、生産性だけでなく読みやすさも重視しています)

以下はmethod call syntaxの例です。

# 両方共同じ意味
writeLine(stdout, "Hello!!")
stdout.writeLine("Hello!!")

演算子

Nimで演算子は普通の関数定義で演算子をバッククォートで囲むことによって定義できます。

type MyInt = object
  value: int

proc newMyInt(value: int): MyInt =
  return MyInt(value: value)
proc `+`(left, right: MyInt): MyInt =
  return newMyInt(left.value + right.value)
proc `$`(myint: MyInt): string =
  return $myint.value

echo newMyInt(100) + newMyInt(50)

上記のコードは自作の型であるMyIntに新しい演算を定義しているコードです。

method call syntax、演算子オーバーロード、そして前回のdistinctを使うと新しい単位を定義するようなこともできます。

type Ether = distinct int

proc `+`(left, right: Ether): Ether {.borrow.} # borrowは元の関数を借りてくる付加情報(プラグマ)
proc `$`(ether: Ether): string {.borrow.}

echo 50.Ether + 600.Ether

上記は新しい単位エーテルを定義している様子です。(ゲームとかに使うと面白そうですね)

まとめ

以上がNimの関数についてです。Nimの関数はDSL的に使うこともでき、とても読みやすいと思います。
DSLについてはNimの重要な機能であるtemplateやmacroと組み合わせて真価を発揮するものなので、それについてもいずれ記事を書きたいと思います。

型の定義

Nimの型定義についてです。

Nimには様々な型の定義方法があります。
型定義の種類として、

  • enum
  • object
  • ref object
  • ptr object
  • 別名
  • distinct

があります。

他にも型の特性を変えるものとして、not nilがあります。

定義方法

型定義の構文は、

type 型名 = 型定義の種類

です。
型定義の種類に上記のenumなどを記述します。

enum

enumは列挙型です。
NimのenumC言語enumと似ています。

type Direction = enum
  North
  East
  South
  West
echo North

しかし、C言語と違うのはそれぞれに値を割り当てることができることです。

type Direction = enum
  North = "N"
  Eest = "E"
  South = "S"
  West = "W"
echo North

{.pure.}をつけることによって、どの列挙型に属すのかを記述するのを強制させることができます。

type Direction {.pure.} = enum
  North
  East
  South
  West
echo North # error!
echo Direction.North # works!

object

objectはいわゆる構造体で、デフォルトでスタック割当されます。

type GameObject = object
  x: int
  y: int
var obj = GameObject(x: 1, y: 5)
echo obj

ref object

ref objectはヒープ割当される構造体です。

type GameObject = ref object
  x: int
  y: int
var obj = GameObject(x: 1, y: 5)
echo obj.x, ":", obj.y

refとobjectは別のキーワードなので、既存の型のref版を作ることもできます。

type GameObject = object
  x: int
  y: int
type PGameObject = ref GameObject
var pobj = PGameObject(x: 1, y: 5)
echo pobj.x, ":", pobj.y

object型とref型の両方で使える関数をorを使うことで作ることもできます。

type GameObject = object
  x: int
  y: int
type PGameObject = ref GameObject

proc echoGameObject(obj: GameObject or PGameObject) =
  echo obj.x, ":", obj.y

var obj = GameObject(x: 1, y: 5)
var pobj = PGameObject(x: 2, y: 5)
echoGameObject(obj)
echoGameObject(pobj)

ptr object

ptr objectもヒープ割当される構造体ですが、GCによって追跡されません。よって通常はref objectを使うことが推奨されます。しかし、場合によっては低レベルまで制御して最適化することが求められるので、こういったものも用意されています。

type RawGameObject = ptr object
  x: int
  y: int
var rawobj = cast[RawGameObject](alloc(sizeof(RawGameObject)))
rawobj.x = 1
rawobj.y = 5
echo rawobj.x, ":", rawobj.y
dealloc(rawobj)

Nimはシステムプログラミングを明確に意識しているので、こういった低レベル操作もできるようになっています。

ジェネリクス

type Buffer[T] = ref object
  data: seq[T]

var buf = Buffer[int](data: @[1, 2, 3, 4, 5])
echo buf.data

NimのジェネリクスC++のようにテンプレート系のジェネリクスです。そのためエラーメッセージが読みにくいという欠点があるので注意です。

別名

型に別名を付ける方法です。おそらくあまり使うことは無いと思いますが一応。

type IntSeq = seq[int]

proc echoIntSeq(data: seq[int]) =
  echo data

var a: IntSeq = @[1, 2, 3, 4, 5]
var b: seq[int] = @[1, 2, 3, 4, 5]
echoIntSeq(a)
echoIntSeq(b)

あくまで別名を付けるだけなので、元の型と同じものとして扱うことができます。

distinct

distinctは既存の型から新しい型を作ります。なので、上記の別名とは違い、同じものとして扱うことはできません。

type Index = distinct int

var a = 1
var b: Index = 2 # error!
var b: Index = Index(2) # works!

not nil

not nilはその型がnilになることを防ぐ機能です。具体的には、objectが初期化されずに生成されることを防いだりするのに使います。

type GameObject = object
  x: int
  y: int
type PGameObject = ref GameObject not nil

var pobj: PGameObject # error!
var pobjinit: PGameObject = PGameObject(x: 1, y: 5) # works!

まとめ

Nimの型は様々な定義方法があります。ゆるい型付けとしても扱えますし、堅めの型としても扱えます。それを柔軟と言うか一貫性が無いと言うかは人によると思いますが、面白い型の扱い方だと思います。

Nimのデータ型

Nimのデータ型についてです。

基本的なデータ型

Nimの基本的なデータ型としては、

  • int
    • int8
    • int16
    • int32
    • int64
  • uint
    • uint8
    • uint16
    • uint32
    • uint64
  • float
    • float32
    • float64
  • bool
  • char
  • string
  • cstring

などがあります。 先頭にuがついているものはCでいうunsignedで、末尾の数字はビット数を表しています。

コンテナ型

そしてそれらを包含するコンテナ型があります。Nimのコンテナ型はそれぞれが使いやすく、演算子オーバーロードがあり、シンタックスシュガーが用意されているのでとても書きやすいです。

コンテナ型でよく使うものとしては、

  • array
  • seq
  • varargs
  • openarray
  • Table

ちなみに下記の例では分かりやすさのため変数に型を明示的に付けているものもありますが、この程度ならNimのコンパイラ型推論してくれるので省略可能です。

array

arrayは静的配列です。

var arr: array[5, int] = [1, 2, 3, 4, 5]

seq

seqは動的配列です。@をつけることで作成可能です。

var arr: seq[int] = @[1, 2, 3, 4, 5]

varargs

varargsは可変長引数に使います。 ちなみに@をつけることでseqに変換可能です。

proc echoVariadic(args: varargs[int]) =
  echo @args
echoVariadic(1, 2, 3, 4, 5)

さらに、varargsで可変長引数を受け取る際に、任意の変換処理を行うことができます。

proc toInt(value: string): int =
  case value
  of "1":
    return 1
  of "2":
    return 2
  else:
    return 0
proc echoVariadic(args: varargs[int, toInt]) =
  echo @args
echoVariadic(1, 2, "1", "2")

openarray

openarrayは配列を汎用的に扱う型です。
例えば、arrayとseq両方で使える関数を作ったりするのに使えます。

proc echoHead(arr: openarray[int]) =
  echo arr[0]
echoHead([1, 2, 3, 4, 5])
echoHead(@[1, 2, 3, 4, 5])

Table

Tableはハッシュテーブルで、標準ライブラリtablesをimportすることで使えます。
他にも順序が付いたOrderedTableなどもあります。

var agetable = initTable[string, int]()
agetable["genji"] = 35
agetable["hanzo"] = 38
agetable["mercy"] = 37
echo agetable["genji"]

ポインタ型

  • pointer
  • ptr
  • ref

pointer

pointerは生ポインタ型で、Cでいうvoid*です。基本的にはCとの連携の際に使うもので、Nimに閉じたプログラムを書く際に必要になることは無いと思います。

ptr

ptrは型がついたpointerです。これも基本的にはCとの連携の際に使うことが多いでしょう。

ref

refも型がついたpointerなのですが、ptrとrefはGCで管理されるかどうかの違いがあります。
ptrはGCで管理されず、refはGCで管理されます。

まとめ

以上がNimのデータ型でよく使うものです。ただし後半のポインタ型はデータ定義のobjectなどと密接に関係してくるもので、少し分かりにくい説明になったと思います。データ定義などの部分はまた別の記事で書きたいと思います。

Nimの基本構文

まず、他の言語にもあるような基本的な構文を紹介していきます。

Hello World

まずは最初の一歩のHelloWorldです。

echo "Hello World!!"

これは、

echo("Hello World!!")

これと同じ意味です。
Nimでは、rubyのように関数呼び出しの場合に括弧を省略することができます。しかし、場合によっては括弧を付ける必要があるのでそこは注意です。

変数、定数、コンパイル時定数

それぞれ、var(変数)、let(定数)、const(コンパイル時定数)になります。

var name = "Hello!!"
echo name
name = "Rewrite!!"
echo name
let name = "Hello!!"
echo name
const name = "Hello!!"
echo name

値が明示の場合、型推論がされます。しかし、値が無い(Nimの場合はnil)場合、型宣言が必要です。

var name: string
name = "Hello!!"

条件分岐

構文としてはPythonのようなif elif elseです。 真偽値はtrue boolです。

if true:
  echo "True!!"
else:
  echo "False!!"

Nimは構造を表現する文法として、Pythonのようなインデント形式を採用しています。基本的にはスペース2つで表現します。

他にも、case of構文があり、

var name = "hello"
case "hello"
of "hello":
  echo 1
of "world":
  echo 2
else:
  echo 3

のように書けます。

他にもwhenがありますが、これはコンパイル時分岐です。

繰り返し構文

繰り返し構文は、for whileがあります。

for i in 1..5:
  echo i
var i = 0
while i < 5:
  echo i
  i += 1

for whileの両方でbreak continueが使えます。

..はいわゆるRangeです。

配列をループする際にも、

var arr = [1, 2, 3, 4, 5]
for val in arr:
  echo val

このように描けます。

indexで回すこともできます。

var arr = [1, 2, 3, 4, 5]
for i in 0..arr.len-1:
  echo arr[i]

しかし、この場合には..<演算子で、

var arr = [1, 2, 3, 4, 5]
for i in 0..<arr.len:
  echo arr[i]

のように書くこともできます。

関数

関数はNimでは手続きと言われており、構文としてはprocになります。

proc add5(val: int): int =
  return val + 5

関数宣言では型の省略はできません。

returnで戻り値を返していますが、他に戻り値を返す方法として、resultに代入するという方法もあります。

proc add5(val: int): int =
  result = val + 5

まとめ

以上がNimの基本構文になります。しかしNimはその他の機能が豊富なのでそれはまた別の記事に描きたいと思います。