Flat Leon Works

アプリやゲームを作ってます。

【Nim】個人的逆引きリファレンス

Nimの勉強を兼ねて逆引きリファレンスとしてまとめてみようと思います。(随時更新)

この記事は「Nim Advent Calendar 2017」の記事として登録させてもらっています。

注意:

  • 使用しているNimのバージョンは0.17.0です
  • Nimに詳しいわけではないので変な書き方をしているかもしれません
  • 個人的な解釈をしていたりするので間違っているかもしれません
  • いきなりこの逆引きリファレンスを見るのではなく、先に公式マニュアルなどを一通り読むことをおすすめします

Nimを普通に実行する : nim c -r Nimソースファイルパス

(コマンドライン)

# 普通に実行
nim c -r Nimソースファイルパス

# 余分な情報を出力しない場合
nim c -r -hints:off Nimソースファイルパス

# プログラムに引数を渡したい場合は、最後に追加する
nim c -r -hints:off Nimソースファイルパス 引数1 引数2...

ドキュメント :
Compiler Usage

標準出力を行う : echo

echo( "aaa" )
echo( 123 ) # 自動で文字列変換される($演算子が呼ばれる)
echo( 123, 456, 789 ) # 複数引数を与えることができる(連結される)
echo "aaa" # プロシージャなので()を省略できる

debugEcho( "aaa" ) # debugEchoはデバッグ時にのみ機能する

writeLine( stdout, "aaa" ) # こういう方法もある(echoは内部でwriteLineを使っている)

ドキュメント :
echo
debugEcho writeLine

Nimのバージョン情報を取得する : NimVersion

echo NimVersion # 0.17.0

ドキュメント :
NimVersion

コマンドライン引数を取得する : os.commandLineParams()

import os

# コマンドライン引数を個別に取得(0番目はプログラム名)
for i in 0..os.paramCount():
    echo os.paramStr(i)

# まとめて取得する(0番目はプログラム名ではない)
for i in os.commandLineParams():
    echo i

ドキュメント :
commandLineParams

型を文字列化する

typetraits.nameプロシージャを使う

import typetraits

echo typetraits.name( int ) # int

proc echoType[T]() = echo( typetraits.name( T ) )
proc echoType[T](x:T) = echo( typetraits.name( T ) )

echoType[int]() # int
echoType[string]() # string
echoType(10) # int
echoType("hoge") # string

# 型名じゃない場合はtype演算子を使ってtypedesc型へ変換してから呼ぶ

echo typetraits.name( type( 1 + 3 ) ) # int

ドキュメント :
`type`
name

プロシージャの引数で型(型名)を受け取る

引数の型としてtypedescを指定すると、型(型名)で受け取れる。この場合、仮引数名は型を表すようになる。

import typetraits
 
proc echoType(x:typedesc) =
    var hoge: x # xは型を表す
    echo typetraits.name( x ) # typetraits.nameも引数がtypedescになっているので型を渡せる
 
echoType(int) # 出力:int
int.echoType  # メソッド構文も使える

# typedescは型しか受け付けないので、普通の式を渡してもコンパイルエラーになる
# echoType(1+2) # エラー
echoType(type(1+2)) # type演算子を使えば一応渡せる

# 普通の型を受け取るプロシージャをオーバーロードすることで、どちらでも渡せるようになる
proc echoType[T](x:T) = echo typetraits.name( T )

echoType(int)   # 出力:int
echoType(1+2) # 出力:int

ちなみに、typedescを変数の型として指定することはできないようだ

var x: typedesc # コンパイルエラー

ドキュメント :
typedesc

式の型を得る

var x : type( 1 + 2 ) # x は int型になる

ドキュメント :
Type operator

変数のアドレスをuint64型で取得する

addr演算子で取得したptr T型をuint64型へキャストすればよさそう(正しくアドレスが取れているのかどうかは不明…)

proc getAddrInt[T](x:var T): uint64 =
    cast[uint64](addr(x))

var hoge: int
var hoge2: string

echo getAddrInt( hoge )
echo getAddrInt( hoge2 )

ドキュメント :
`addr`

基本文法

コメントを記述する

# コメント

## ドキュメント用コメント

#[
複数行コメント
    #[
    ネスト可能
    ]#
]#

ドキュメント :
Comments
Multiline comments

変数を定義する

var 識別子: 型名
var 識別子: 型名 = 初期値
var 識別子 = 初期値
var hoge: int # 変数定義
var hoge2: int = 10 # 変数定義(初期値指定)
var hoge3 = 10 # 初期値指定する場合は型の記述を省略可能
var a, b, c: int # まとめて定義
var # varセクションでまとめて定義
    x, y, z: int
    name: string

# var ではなく let を使うことで変更不可の変数を定義することができる
let l_hoge = 1
# l_hoge = 2 # コンパイルエラーになる

ドキュメント :
Var statement
let statement

定数を定義する

const HOGE = "hoge"
var hoge2 = "hoge2"
const HOGE2 = hoge2 # 定数なのでコンパイル時に値が確定しないものは設定できない

ドキュメント :
Const section

文字列リテラルを利用する

# 文字列リテラル(String literal)
echo( "abc" ) # 出力:abc

# raw文字列リテラル(Raw string literal)
# 文字がエスケープされない
echo( r"newline character is '\n'" ) # 出力:newline character is '\n'

# 文字列リテラル(Triple quoted string literal)
# 文字がエスケープされない。改行ができる。
echo( """a
b
c
""" )
# 出力:
# a
# b
# c

# pythonとは違い''は文字列リテラルとして使えない
# echo( 'abc' )

ドキュメント :
String literals
Triple quoted string literals
Raw string literals

数値リテラルを利用する

import typetraits # nameプロシージャのため

echo( 123 ) # 123

# 途中の_は無視される
echo( 123_456 ) # 123456

# 浮動小数点数
echo( 123.0 ) # 123.0
echo( 123e1 ) # 1230.0
echo( 123.0e1 ) # 1230.0

# 16進数
echo( 0xFF ) # 255

# 2進数
echo( 0b0010 ) # 2

# 8進数
echo( 0o0010 ) # 8

# サフィックスで型を指定できる
echo typetraits.name( type( 123 ) ) # int
echo typetraits.name( type( 123'i8 ) ) # int8
echo typetraits.name( type( 123'f ) ) # float32
echo typetraits.name( type( 0xFF'f ) ) # float32

ドキュメント :
Numerical constants

プロシージャを呼び出す

echo( "aaa" ) # 普通にプロシージャ呼び出し

# コマンド呼び出し構文(Command invocation syntax)
echo 1 # ()を省略できる
echo 1, 2 # ()を省略して複数引数を渡すこともできる

# メソッド呼び出し構文(Method call syntax)
1.echo() # echo(1)と等価。
1.echo( 2 ) # echo(1,2)と等価。
1.echo # コマンド呼び出し構文との組み合わせ。echo(1)と等価。
1.echo 2 # コマンド呼び出し構文との組み合わせ。echo(1,2)と等価。

# 名前付き引数
proc test_proc( a:int, b:string ) =
    echo a, b
test_proc( 1, "hoge" )
test_proc( 1, b="hoge" ) # 名前付き引数

ドキュメント :
Procedures
Command invocation syntax
Method call syntax

プロシージャを定義する

proc 識別子(仮引数名:型名, 仮引数名:型名...): 戻り値型 =
    プロシージャ本体
# 普通に定義
proc myProc1(name: string): bool =
    return name == "aaa"

proc myProc2(name: string): bool = return name == "aaa" # 1行なら改行不要
proc myProc3(name: string): void = echo( name ) # 戻り値なし
proc myProc4(name: string) = echo( name ) # 戻り値なしなら戻り値型指定は省略できる
proc myProc5(name: string): bool = name == "aaa" # 最後の式は暗黙でreturnされる
proc myProc6(name: string = "aiueo") = echo( name ) # デフォルト引数を設定可能
proc myProc7(name = "aiueo") = echo( name ) # デフォルト引数を設定した場合、型指定は省略できる

proc myProc8(name: string): bool # 宣言のみ
myProc8() # この時点でプロシージャ定義がされていなくても宣言があれば呼び出せる
proc myProc8(name: string): bool = return true # 宣言したらプロシージャ定義は必要(ないとコンパイルエラー)

proc myProc9(name: string): bool =
    result = true # 暗黙のresult変数が利用できる(暗黙にreturnされる)
    echo( name )

プロシージャの他の特徴としては以下のものがある

  • 仮引数は変更不可
  • プロシージャ呼び出し側に戻り値を必ず受け取らせるかどうかを設定可能(デフォルトでは有効) : {.discardable.}
  • オーバーロード可能
  • クロージャを作成可能(プロシージャ内部で定義されたプロシージャはクロージャとなる)
  • 無名プロシージャを作成可能
  • 動的ディスパッチに対応したプロシージャ(C++でいう仮想関数)を定義することが可能
  • イテレータを定義可能
  • インライン化指定可能 : {.inline.}
  • 各種呼び出し規約を指定可能
  • モジュール外部に公開するかどうかを設定可能(Export marker)
  • 引数を参照で受け取ることが可能(ここでいう参照とはC++的な参照)
  • 戻り値を参照で返すことが可能(ここでいう参照とはC++的な参照)
  • ジェネリクスプロシージャを定義可能(C++でいうテンプレート関数)
  • その他特殊なプロシージャを定義可能
    • 暗黙の型変換用プロシージャ
    • 各種オペレーター
    • ユーザー定義オペレーター

ドキュメント :
Procedures

式の場所で文を記述する

(文;文;式)

最後の式が評価結果になる。

const fac4 = (var x = 1; for i in 1..4: x *= i; x) 

ドキュメント :
Statements and indentation

if文を利用する

if name == "abc":
    echo( "ABC" )
elif name == "defg":
    discard # Pythonのpass文のような用途としてもdiscard文が使える
else:
    echo "???"

# if文内は個別のスコープになる
if (var hoge = true; hoge):
    echo( hoge )
else:
    # echo( hoge ) # hogeはスコープ外 
    discard
# echo( hoge ) # hogeはスコープ外 

ドキュメント :
If statement

case文を利用する

C言語のSwitch文のようなもの。

case 式
of 式:
    文
of 式:
    文
else:
    文
case name
of "abc":
    echo( "ABC" )
of "def", "ghi": # ,で複数条件を指定できる
    discard # Pythonのpass文のような用途としてもdiscard文が使える
else: discard

# case以降でインデントが許されている(インデントする場合はcaseの後に : が必要)
case name:
    of "abc":
        echo( "ABC" )
    of "def", "ghi": # ,で複数条件を指定できる
        discard # Pythonのpass文のような用途としてもdiscard文が使える
    else: discard

ドキュメント :
Case statement

for文を利用する

for 要素を受け取る変数 in イテレータ:
    文

for インデックスを受け取る変数, 要素を受け取る変数 in イテレータ:
    文
for i in countup(1, 10):
    if false: continue # continue文が使える
    if false: break # break文も使える
    echo i
for i in 1..10: # 1..10はcountup(1,10)と同じ
    echo i
for i in 1..<11:
    echo i

type MyEnum = enum A, B, C

for i in MyEnum: # enum型に含まれるenum値を走査できる
    echo i

for index, value in ["a","i","u","e","o"]: # インデックス付きで走査も可能(できない型もある)
    echo "Index=", index, " Value=", value

for文に渡したものがイテレータでない場合、itemsイテレータ(変数が1個の場合)かpairsイテレータ(変数が2個の場合)が暗黙に呼ばれる。このとき、itemsイテレータもしくはpairsイテレータが定義(オーバーロード)されていない場合、コンパイルエラーになる。この仕組により、array型やseq型をfor文で利用できるようになっている(array型やseq型のitems/pairsイテレータが標準で実装されている)。

標準で実装(オーバーロード)されているitems/pairsイテレータ

  • openArray[T]
  • array[IX, T]
  • set[T]
  • cstring
  • typedesc[enum]
  • Slice[T]
  • seq[T]
  • string

標準で実装されているイテレータ(詳細はIterators(ライブラリリファレンス)を参照)

  • countdown
  • countup
  • `..`
  • `||`
  • items
  • mitems
  • pairs
  • mpairs
  • fields
  • fieldPairs
  • lines
  • `..<`

for i in 0..5:のように書けるのは`..`演算子イテレータになっているから。

ドキュメント :
Iterators and the for statement
For statement(チュートリアル)
Iterators(ライブラリリファレンス)

while文を利用する

var i = 1
while i <= 10:
    if false: continue # continue文が使える
    if false: break # break文も使える
    echo i
    inc( i )

ドキュメント :
While statement
inc

block文を利用する

block:
    var x = "hi"

#[ 変数xはスコープ外なのでアクセスできない
echo x # エラー
]#

block myBlock: # ブロックに名前を付けることもできる
    discard

ドキュメント :
Block statement

break文を利用する

break文は for文, while文, block文 で利用できる

for i in 0..5:
    if i == 3: break

# ブロック名を指定して抜け出すこともできる
block myBlock:
    block myBlock2:
        if true:
            break # myBlock2から抜け出す
        else:
            break myBlock # myBlockから抜け出す

ドキュメント :
Break statement

when文を利用する

C言語における、#ifに近い機能。when文はコンパイル段階で条件分岐が行われる。そのため条件式は定数式しか受け付けない。また、ブロックスコープは発生しない。

when true:
    echo "aaa"
    var x = 10
elif true:
    apple = 10 # ここは意味解析が行われない(構文解析は行われるので変なこと書くとコンパイルエラーになる)
else:
    discard
echo x # ブロックスコープが発生しないのでxにアクセスできる

ドキュメント :
When statement

イテレータを定義する

イテレータについて

  • イテレータはプロシージャの亜種
  • イテレータは処理を中断(yield)、再開させることができる
  • イテレータではyield文を使用することができる
  • イテレータではreturn文やresult変数は使用できない
  • for文はイテレータを繰り返し呼び出すことでループを行う
  • イテレータはinlineとclosureの2種類がある
  • イテレータはデフォルトではinlineタイプ
  • inlineイテレータはfor文でしか使うことができない
  • closureイテレータはfor文以外にも変数として扱うことが可能
  • イテレータはプロシージャと別の名前空間になっているので、それぞれ同名のものを定義できる
    • 標準ライブラリのstrutils.splitはこれを利用して、for文ではイテレータのsplit、その他ではプロシージャのsplitが呼ばれるようになっている
iterator countup(a, b: int): int =
    var res = a
    while res <= b:
        yield res
        inc(res)
for i in countup(1, 10):
    echo i

ドキュメント :
Iterators and the for statement
Iterators(チュートリアル)

プロシージャル型を利用する

プロシージャル型はプロシージャへの参照のようなもの。プロシージャル型の変数を使うことでC言語の関数ポインタのような処理が可能になる。

proc test2(x:int):int =
    echo "test2:", x
    x

# プロシージャ名単体はプロシージャル型として評価される
echo( name( type( test2 ) ) ) # 出力:proc (x: int): int{.gcsafe, locks: 0.}

var p2: proc(x:int):int = test2 # プロシージャル型を変数へ代入
var p3 = test2 # 型推論が働くので型を明示的に指定する必要はない
assert p2(77) == 77 # プロシージャのように呼び出せる

ドキュメント :
Procedural type

enum型を定義する

type
    識別子 = enum
        識別子, ...
type
  # enum型を定義
  MyEnum = enum
    A,
    B,
    C,
    D,

# 改行を省略することもできる
type MyEnum2 = enum A, B, C, D,

# 値(ordinal value)を指定することもできる
type MyEnum3 = enum
    one = 1,
    two, # 自動で+1されて2になる
    five = 5 # 順番を飛ばした値を指定することもできるが、ordinal typeではなくなり、incなどのプロシージャが使えなくなる

# enum型の変数を定義
var enumValue: MyEnum 

# enum値は文字列化可能
echo enumValue # A

ドキュメント :
Enumeration types

enum値を数値に変換する

ordプロシージャを使う

type MyEnum = enum A, B, C, D,

var enumValue: MyEnum

echo ord( enumValue ) # 0
echo ord( B ) # 1

ドキュメント :
Enumeration types
ord

enum値を専用のスコープ内に定義する

type MyEnum {.pure.} = enum A, B, C, D,

# echo A # Aではアクセスできない
echo MyEnum.A

ドキュメント :
Enumeration types

enum値の文字列表現を指定する

文字列を指定すると、文字列表現の際にその文字列が使用される。また、タプルで指定することで値(ordinal value)と文字列表現の両方を指定出来る。

type MyEnum = enum
    A = "A team",
    B = "B team",
    C = "C team",
    D = (8, "D team"),

echo A # A team
echo ord( A ) # 0

echo D # D team
echo ord( D ) # 8

ドキュメント :
Enumeration types

Subrange型を利用する

Subrange型はOrdinal型の範囲を制限した型。

  • 元となったOrdinal型をBase型と呼ぶ
  • Subrange型のサイズはBase型と同じになる
  • Subrange型への代入はコンパイル時または実行時にチェックが入る
  • Base型からSubrange型への代入、またはその逆は許可されている
  • ビット演算のandは一方の項が定数の場合、Subrange型に変換される(例:'x and 3'の評価結果は'range[0..3]'型になる)
  • Subrange型を作るにはrange型を使う
var hoge: range[0..5]
hoge = 1
# hoge = 6 # 値の範囲外なのでエラー

ドキュメント :
Subrange types

型に別名を付ける

type
    MyInt = int
    MyInt2 = int
 
var mi: MyInt
var mi2: MyInt2
mi = mi2 # 型名は違うけど、実際の型はintなので代入できる

ドキュメント :
Type sections

既存の型から独自の型を作る

type
    MyInt = distinct int
    MyInt2 = distinct int
 
var mi: MyInt
var mi2: MyInt2

# mi = mi2 # 別の型なので代入できない

ドキュメント :
Distinct type

固定長配列(array型)を利用する

(array型)
array[要素数,要素型]
array[インデックス範囲,要素型]

(Array型のオブジェクトを作成(Arrayコンストラクタ))
[要素1, 要素2, ...]
# 5要素のstringの配列を作成
var x : array[5, string]

# Arrayコンストラクタ[]で作成した値を代入
x = ["a","b","c","d","e"]

# for文で走査
for value in x:
    echo value

# index付きで走査
for index, value in x:
    echo "Index=", index, " Value=", value

# その他のArray作成例

var x2 : array[1..5, string] # 範囲指定で作成(0オリジンでない配列を作成可能)

type MyEnum = enum A, B, C

var x3 : array[MyEnum, MyEnum] # 範囲指定としてenum型を指定

# これらはx3と同じ範囲指定をしたことになる(同じ型として扱われる)
var x4 : array[low(MyEnum)..high(MyEnum), MyEnum]
var x5 : array[ord(high(MyEnum))+1, MyEnum]
var x6 : array[0..2, MyEnum]
var x7 : array[3, MyEnum]

標準で定義されているプロシージャ

  • high : 最大のインデックスを取得
  • low : 最小のインデックスを取得
  • len : 要素数を取得
  • min : 全要素の中から最小のものを取得
  • max : 全要素の中から最大のものを取得
  • `==` : 比較
  • `@` : seq型へ変換したものを取得
  • contains : 特定の値が要素に含まれるかどうかを取得
  • `` : 要素を取得
  • `=` : 要素へ代入
  • items : for文走査用
  • mitems : for文走査用
  • pairs : for文走査用
  • mpairs : for文走査用

ドキュメント :
Array and sequence types

【おまけ】Arrayのジェネリクスパラメータは何を受け付けるのか

Arrayは以下のようにジェネリクスとして定義されている(system.nim)

array[I, T]

TはArrayの要素の型を表すので任意の型を指定できるとして、Iには何を指定できるのか。他の多くのNimの組み込み型と同様にarrayジェネリクスはNimコードとしては実装されておらず、コンパイラコード内で実装されている(ややこしいことにコンパイラはNimで書かれているのである意味Nimコードで実装されているとも言える)。そこでコンパイラコード内を探してみたところ、Iについてチェックを行っていると思われるコード(semtypes.nim)を見つけた。

簡単にまとめると

  • range型(a..b形式で記述されているもの)
  • int型
  • 定数式

これらのものがIに指定可能だと思われる。そして、どの形式でも最終的にはrange型へ変換しているようだった。

動的配列(seq型)を利用する

(seq型)
seq[要素型]

(seq型のオブジェクトを作成(arrayコンストラクタで作成したものを@演算子でseq化している))
@[要素1, 要素2, ...]
var
    x: seq[int]
x = @[1, 2, 3, 4, 5, 6]
for value in x: # Pythonのようにfor文で走査できる
    echo value
for index, value in x: # 2つのパラメータだとインデックスと値を取得できる
    echo "Index=", index, " Value=", value

基本的にはarray型の上位互換なので「固定長配列(array型)を利用する」も参照。seq型の初期値は空配列(@[])ではなく、nilなことに注意。

標準で用意されているseq型のプロシージャ(array型にはないもの)

  • newSeq : 長さを指定して新しいseqを作成する。変数を引数にとり、新しいseqはその変数へセットされる
  • newSeq : 長さを指定して新しいseqを作成する。新しいseqは戻り値として渡される
  • newSeqOfCap : キャパシティを指定して長さ0の新しいseqを作成する。新しいseqは戻り値として渡される
  • setLen : 長さを変更する
  • add : 要素を追加する
  • del : 末尾要素を指定のインデックスの位置へ移し上書きする。(結果的に要素数は1つ減る)
  • delete : 指定のインデックスの要素を取り除く。(delよりこっちの方が一般的な挙動)
  • insert : 指定のインデックスの位置に要素を追加する
  • isNil : seqがnilかどうかを取得
  • `&` : seqを連結する
  • pop : 末尾の要素を削除する
  • shallow : 代入時にshallowコピーを行うようにする。
  • safeAdd : 要素を追加する。seqがnilの場合には最初にseqを作成する。

ドキュメント :
Array and sequence types

可変個引数を利用する

varargs[引数の型]
varargs[引数の型,変換用のプロシージャル型]
  • プロシージャで可変個引数を扱うには、引数の型としてvarargsを使う
  • 渡された可変個引数はarrayに変換される
  • varargsはプロシージャの最後の引数としてしか指定できない
  • varargsの2番目のジェネリクスパラメータとしてプロシージャル型を渡すことで、可変個引数の各要素にたいして任意の変換処理を行わせることができる
  • 可変個引数としてarray型を渡すと自動で展開されて個別の引数として渡される
    • array型のまま渡したい場合は、その値をarrayコンストラクタでさらにくくればよい
    • マニュアルにはvarargs[typed]を使えばarray型の自動展開を回避できると書いてあったが、試してみるとコンパイルエラーになった(procではなくtemplate用?)
proc test(a: varargs[string]) =
  for s in a:
    echo s
    
test( "a", "i", "u" )
# 出力:
# a?
# i?
# u?

test( ["a", "i", "u"] ) # array型は自動展開されるので↑と同じ引数を渡したことになる
# 出力:
# a
# i
# u

proc test2[T](a: varargs[T]) =
  for s in a:
    echo repr(s)

test2( "a", "i", "u" )   # Tはstring型になる
test2( ["a", "i", "u"] ) # Tはstring型になる

# arrayのまま渡したい場合はさらにarray化する
test2( [["a", "i", "u"]] ) # Tはarray[string]型になる

# 変換プロシージャを用意(末尾に?を追加する例)
proc t(x:string): string = x&"?"

# 変換処理を指定
proc test3(a: varargs[string,t]) =
  for s in a:
    echo s
    
test3( "a", "i", "u" )
# 出力:
# a?
# i?
# u?

ドキュメント :
Varargs

タプルを利用する

  • Nimのタプルは他のプログラミング言語と違いフィールド名を付けることができる
  • フィールド名が違う場合、違う型として扱われる
  • タプルからタプルに代入した場合、それぞれの要素がコピーされる
  • フィールド名と型とその順番が同じものは同じ型として扱われる
  • フィールド名が無いタプル型も作成することができる
  • フィールド名なしのタプル型とフィールド名ありのタプル型は、型と順番され合っていれば代入などの操作が可能(これが仕様かどうかは不明)
【タプル型】
tuple[フィールド名: 型, フィールド名: 型,...] # フィールド名ありの場合
(型,型,型,...) # フィールド名なしの場合
# Object型風の型定義も可能
type
    新規型名 = tuple
        フィールド名: 型
        フィールド名: 型
        ...

【タプルコンストラクタ】
(フィールド名:値, フィールド名:値)
(値, 値)

【フィールドへのアクセス】
タプルオブジェクト.フィールド名
タプルオブジェクト[フィールドインデックス]

【タプルの中身を変数で受け取る(tuple unpacking)】
var (変数名1, 変数名2, 変数名3, ...) = タプルオブジェクト
var (変数名1, _, 変数名2, ...) = タプルオブジェクト # 不要な要素には _ を使用することで必要な要素だけ受け取ることができる
var x = (id:"hoge",age:10)
var y = ("hoge2",10) # コンストラクタでのフィールド名は省略可能
var z = (id:"hoge3",height:150)
var w = (age:30,id:"hoge4")
x = y # 型と名前、そしてその順番が一致しているので代入可能
# x = z # 名前が一致していないものがあるので代入不可
# x = w # 型と名前が一致していても順番が一致していないので代入不可

# フィールドアクセス
echo x.id # hoge2

# []でもアクセスできる
echo x[0] # hoge2

ドキュメント :
Tuples and object types
Tuple unpacking
Tuples(チュートリアル)

Object型を利用する

  • Object型はタプルに以下のようなオブジェクト指向プログラミングの機能を追加したようなもの
    • 型の継承
    • 非公開フィールド
    • 独自の型(暗黙でdistinctが付くイメージ?)
  • Object型名()でオブジェクトを構築できる(object construction expression)
    • 引数を渡す場合はフィールド名指定が必須
    • Object型がref付きの場合、暗黙にsystem.newが呼ばれる
  • {.final.}プラグマで派生を禁止させることができる
  • RootObj型を継承していないObject型は暗黙に{.final.}プラグマが付く。またフィールドはすべて公開される。
  • refを付けない場合、派生型を基底型へ代入すると派生型のフィールドが切り捨てられてしまうことに注意(C++でいうオブジェクトスライス問題)
  • 非公開フィールドは同じモジュール内ならアクセス可能
  • C++でいうメンバ関数というものは存在しない。ただし、メソッド記法を使うことでプロシージャをメンバ関数のように呼び出すことは可能
【Object型定義】
type
    # 新規Object型を定義する
    新規オブジェクト型名 = object
        フィールド名: 型名 # 公開フィールド
        フィールド名: 型名 # 公開フィールド
        ...
    # 派生型定義
    新規オブジェクト型名 { .final. } = object of 基底型 # { .final. }は任意
        フィールド名*: 型名 # 公開フィールド
        フィールド名: 型名   # 非公開フィールド
        ...

【Object型構築】
var 変数名 = オブジェクト型名(フィールド名:値,フィールド名:値,...)
type
    # Object型を定義
    MyObj = ref object of RootObj
        x: int
        y: int
    # 派生型を定義
    MyObj2 = ref object of MyObj
        z: int

proc test(hoge:MyObj) = echo hoge.x, ", ", hoge.y

# オブジェクト構築
var obj: MyObj = MyObj(x:1, y:2)
var obj2: MyObj2 = MyObj2(y:20, z:30)

# メンバ関数のように呼び出すことが可能
obj.test()
obj2.test()

ドキュメント :
Tuples and object types
Object construction

Set型

  • いわゆる集合型(同じ値は複数格納できない)
  • 要素の型は以下のものに限られる
    • int8-int16
    • uint8/byte-uint16
    • char
    • enum
  • 集合型というよりフラグ管理を想定しているっぽい
【Set型】
set[要素の型]

【Set型コンストラクタ】
{値,値,...}
{値..値,...}
var x: set[int8]
 
var baisu = {3,6,9,12,15,18,21,24,27,30,33,36,39} # 3の倍数
var three = {3,13,23,30..39} # 3のつく数字
 
var aho = baisu + three # 和集合を作成
 
for i in 0..40:
    if i in aho: echo i, " !!!"
    else: echo i

Set型のプロシージャ

  • `==` : 比較
  • `<=` : 比較
  • `<` : 比較
  • incl : 含める(追加する)
  • excl : 除外する(削除する)
  • card : 要素数を取得(正確には集合の濃度?)
  • `*` : 積集合を取得
  • `+` : 和集合を取得
  • `-` : 差集合
  • contains : 含まれるかどうかを取得
  • `$` : 文字列化
  • items : for文用
  • `in` : 含まれるかどうかを取得
  • `notin` : 含まれないかどうかを取得

ドキュメント :
Set type

参照を利用する

  • 変数はデフォルトでは参照ではなく値(代入はコピーが発生する)
  • 型名にrefを付けることで参照型の変数を定義できる
  • 参照型はGC対象のヒープ上のオブジェクト(traced object)への参照を持つ(それ以外のオブジェクトへの参照は持てない)
  • 参照型の参照先を得る(デリファレンスする)には[]演算子を使う
  • 参照型への.演算子[]演算子(インデックス演算子)は暗黙にデリファレンスされる
  • プロシージャの最初の引数として参照型を渡す場合、暗黙にデリファレンスされる(ただし{.experimental.}プラグマ指定が必要)
  • traced objectを作成するにはnewプロシージャを使う
  • 参照型がnilの場合、その参照はどこも指していないことを表す
  • 参照型で値型(非参照型)の参照を行うことはできない(と思われる)
# 参照型変数を定義
var x: ref int

assert x == nil # まだどこも参照していない

x = int.new # newプロシージャでtraced objectを作成し参照型変数へ代入
x[] = 10 # デリファレンスは[]演算子を使う
echo x[] 

# 同じ参照先を指す参照型変数を定義してみる
var x2 = x

x2[] = 20 # 参照先の中身を変更

assert x[] == 20 # xとx2は同じ参照先なのでxの参照先の中身も変更される

# 引数として参照を受け取ることもできる
proc test(a:ref int) = echo a[]

test( x )

#[ 参照ではないのでエラー
test( 10 )
var v = 10
test( v )
]#

ドキュメント :
Reference and pointer types

無名プロシージャを利用する

【無名プロシージャ】
proc(引数リスト): 戻り値型 = 文リスト
# 無名プロシージャを作成し、変数へ代入
var p = proc( x:int ): int =
  return x + 1

echo p( 10 ) # 11

ドキュメント :
Anonymous Procs

クロージャを利用する

  • モジュールトップレベル以外で定義されたプロシージャ(無名プロシージャ、名前付きプロシージャ)はクロージャとなる
  • クロージャクロージャを内包するスコープのローカル変数へアクセスできる
  • キャプチャされた変数は"環境"としてクロージャへの隠し引数として渡される
  • キャプチャされた変数は参照としてアクセスされる
  • "環境"用のメモリはヒープ上に確保されるが、可能な場合はスタック上で確保される
proc getClosure(n:string): proc() =
    var x: int
    var name = n
    return proc() =
        x = x + 1
        echo name, " : ", x
    #[ 普通の名前付きのプロシージャもプロシージャル型として渡せばクロージャになる
    proc hoge() =
        x = x + 1
        echo name, " : ", x
    return hoge
    ]#
    
var c = getClosure( "c" )
var c2 = getClosure( "c2" )

# クロージャはそれぞれの環境を保持しているので、それぞれが呼び出されるたびに数字が増える

c() # c : 1
c() # c : 2
c2() # c2 : 1
c()  # c : 3

ドキュメント :
Closures
Anonymous Procs

ジェネリクスを利用する

  • ジェネリクスC++のテンプレートのようなもの
  • ジェネリクスを使うことで型をパラメータ化したプロシージャ、イテレータ、型を定義することができる
  • 基本的には、定義時、利用時、それぞれで名前のあとに[]で型引数リストを付けるだけ
  • ジェネリクスプロシージャの型引数は、プロシージャの引数から推論が働くので省略できる場合がある(C++のテンプレートと同様)
【ジェネリクスプロシージャ(イテレータ)の定義】
proc プロシージャ名[型引数リスト](引数リスト): 戻り値型 = 文リスト

【ジェネリクスプロシージャ(イテレータ)の呼び出し(とインスタンス化)】
プロシージャ名[型引数リスト](引数リスト)

【ジェネリクス型の定義】
type
    ジェネリクス型名[型引数リスト] = 型定義

【ジェネリクス型の利用】
var hoge: ジェネリクス型名[型引数リスト]
# ジェネリクスプロシージャを定義
proc test[T1,T2](x:T1,y:T2) =
    echo "T1=", typetraits.name(T1), "(", x ,")", " T2=", typetraits.name(T2), "(", y ,")"
 
# ジェネリクスプロシージャを呼び出し(明示的に型引数を与える)
test[int,string](10,"aaa") # T1=int(10) T2=string(aaa)

# ジェネリクスプロシージャを呼び出し(型引数をプロシージャ引数から推論してもらう)
test("bbb",30) # T1=string(bbb) T2=int(30)

#[ ジェネリクスプロシージャは型引数を与えるまで実体が存在しないのでプロシージャル型(関数ポインタ)として扱えない
var p = test
]#
var p = test[int,string] # 型引数を与えて実体化すればプロシージャル型として扱える
p( 40,"ccc" ) # T1=int(40) T2=string(ccc)

# ジェネリクスオブジェクト型を定義
type
    MyObj[T] = ref object of RootObj
        x: T
var o = MyObj[int]()
echo typetraits.name( type( o.x ) ) # int

#[ オブジェクトコンストラクタには型引数の推論はないようだ 
var o2 = MyObj( x:"aaa" ) # コンパイルエラー
]#
var o2 = MyObj[string]( x:"aaa" )
echo typetraits.name( type( o2.x ) ) # string

ドキュメント :
Generics

テンプレートを利用する

  • テンプレートはAST(抽象構文木)を置換できる仕組み
  • C言語でいう関数マクロと似ている(ただし、テキストではなくASTを置換する)
  • テキストの置換ではないので、ASTとして表現できないものをテンプレートの引数として渡すことはできない
  • テンプレートはプロシージャのように定義、呼び出しを行うことができる
  • 演算子形式やメソッド記法での呼び出しにも対応している
    • 実際、組み込み演算子である!=, >, >=, in, notin, isnotはテンプレートで実装されている
  • テンプレートの引数に指定する型として通常の型の他に、untyped,typed,typedescが使える(これらはメタ型(meta types)と呼ばれる)
    • untypedはテンプレート引数の型解決をテンプレート呼び出し時には行わないようにさせるときに使う
    • typedはテンプレート引数の型解決をテンプレート呼び出し時に行うようにさせるときに使う
    • typedescはテンプレート引数として型名を指定させたいときに使う
  • テンプレート呼び出し時に呼び出し式の後に:とそれに続けてブロック文を記述することで、テンプレートの最後の引数としてブロック文を渡すことができる
  • テンプレートでも可変個引数を利用可能。その引数をuntypedにさせたい場合はvarargs[untyped]とする
    • テンプレートでの可変個引数はfor文などで走査することはできない(マクロならできる)
  • テンプレートで展開されたコードのスコープはテンプレートが定義された場所のスコープになる -> Symbol binding in templates
  • テンプレートコード内で``を使うことでテンプレート引数と文字列を結合させることができる(C言語マクロでいう##的な…) -> Identifier construction
  • テンプレート仮引数は同名の外部の識別子(変数名など)を隠蔽する(通常のプロシージャと同じく)
【テンプレート定義】
template テンプレート名(引数リスト): 戻り値型 = 文リスト

【テンプレート呼び出し】
テンプレート名(引数リスト) # 普通に呼び出し

第一引数 テンプレート名 第二引数 # 演算子形式で呼び出し

第一引数.テンプレート名(残りの引数リスト) # メソッド記法で呼び出し

テンプレート名(引数リスト): # :のあとに続くブロック文は最後の引数として渡される
    ブロック文
# 渡されたブロック文全体を文字列として出力するコードを追加するテンプレート
template echoBlock( blockName, x:untyped ): untyped  =
    echo "====== ", astToStr(blockName), " ======"
    echo astToStr( x )
    echo "\n=================="
    x

echoBlock(test): # :を使ってブロック文ごと渡す
    var x: int
    x = 10
    echo x

実行結果

====== test ======

  var x: int
  x = 10
  echo x

==================
10

ドキュメント :
Templates

モジュールを利用する

  • モジュールによってプログラムを分割することができる
  • モジュールは個別のファイルによって定義される(そしてファイル名がモジュール名になる)
  • モジュールごとに名前空間を持つ
  • import文を使うことで他のモジュールにアクセスすることができる
  • モジュールトップレベルに存在する*が付いたシンボルは他のモジュールからアクセスが可能になる
  • export文を使うことで、export文を使ったモジュールがインポートされたときに自動でそのモジュールもインポートされる
【import文】
import モジュール名 # 普通にインポート
import モジュール名 except シンボル名 # 一部のシンボルを除いてインポート
import モジュール名 as 別名 # モジュールをインポートし、別名でアクセスできるようにする
from モジュール名 import シンボル名 # 一部のシンボルのみインポートする

【export文】
export モジュール名

ドキュメント :
Modules