NimチュートリアルPart1 まとめ
NimチュートリアルPart1の個人的要点まとめ。
Ver0.20.0
Lexical Element
- タブ文字はインデントとして識別されない
- systemモジュールはすべてのファイルに暗黙的にインポートされている
-
##
はドキュメントコメント- コメントではあるがSyntax上の制限があり、どこでも書けるわけじゃない
-
#[ ]#
で複数行コメント -
r"hoge\fuga"
は raw文字列-
"""aaa"""
で複数行
-
- 予め
var x: int
で変数宣言しないとx = 1
はできない。 - 複数変数への代入が可能だが ``var x, y = foo()
と記述すると
foo()` が合計で2回呼ばれるので注意
Value Type
- 値型
-
=
での操作は基本的にコピーが渡される- 参照渡しではないため巨大オブジェクトを
=
すると高コストになることがある
ので注意
- 参照渡しではないため巨大オブジェクトを
-
let, const
-
const
はコンパイル時定数- バイナリの
.data
領域に設置されるデータのこと
- バイナリの
-
let
は一度代入されると、変更不可になる。-
let a = readLine()
はOKだがconst
だとエラー
-
Numbers
-
1_000_000
- 可読性のため
_
の挿入が可能
- 可読性のため
1.0e9
-
0x
で16進数 -
0b
で2進数 -
0o
で8進数
case文
-
switch
がない -
else: discard
で明示的にdefault
な処理を入れないとコンパイルできない
case name
of "":
echo "Poor soul, you lost your name?"
of "name", "hoge", "fuga":
echo "foo"
else: discard
discard
-
discard "hogefuga"
でデータを捨てられる - 単文としての
discard
は Python でいうpass
- 何もしない
-
Nim
は暗黙的な関数の返り値の破棄ができない- 返り値が不要なときは
discard someFunc()
と書く必要がある。
- 返り値が不要なときは
-
discard
なしで返り値を破棄できるようにしたい場合はdiscardable
プラグマをつかう
proc p(x, y: int): int {.discardable.} =
return x + y
for文
-
for
のなかのi
は自動的に定義されるため事前宣言が不要
for i in 1..10:
...
for i in 0..<10:
... # 0..9
var s = "some string"
for i in 0..<s.len:
...
for i in 0..^s.len:
... # 長さを超えるのでエラーになる
block
- コードブロックを表す
- ブロックラベルはオプション
- ラベルがある場合は
break
でブロックから抜けることも可能になる
block myblock:
var x:int
...
break myblock # ラベルがあるとbreak先が指定可能
when
- 定数式の比較しかできない
-
if
など違い新しいblock
を生成しない - コンパイラは最初に
true
となったコードしか生成しない- 実質C言語の
ifdef
のようなコンパイル時スイッチのような動作
- 実質C言語の
when system.hostOS == "windows":
echo "running on Windows!"
elif system.hostOS == "linux":
echo "running on Linux!"
elif system.hostOS == "macosx":
echo "running on Mac OS X!"
else:
echo "unknown operating system"
インデント
- Pythonと同様にコードの固まりを表す
-
(
とand
のあとは任意のインデントが可能
# computes fac(4) at compile time:
const fac4 = (var x = 1; for i in 1..4: x *= i; x)
Procedure(関数)
-
result
という変数が暗黙的に関数の最初にゼロ値で宣言されている。-
result
は関数の最後に暗黙的にreturn
される -
return
のみ記述された場合も暗黙的にreturn result
になる。
-
- 関数のパラメータはイミュータブルなので注意
- 引数をミュータブルにしたければ同名の変数を関数内で
var
で再宣言(shadowing) - 最初からミュータブルなパラメータを宣言したければ
var
を引数に追加する- C言語の参照渡しと同じ効果
- 引数をミュータブルにしたければ同名の変数を関数内で
# res, remainder は呼び出し側の値も変わる
proc divmod(a, b: int; res, remainder: var int) =
res = a div b # integer division
remainder = a mod b # integer modulo operation
関数の多重定義
- C++のような関数の多重定義が可能
proc toString(x: int): string = ...
proc toString(x: bool): string =
if x: result = "true"
else: result = "false"
演算子
-
使える文字は基本的に
+ - * \ / < > = @ $ ~ & % ! ? ^ . |
-
Nim
には中置演算子と前置演算子のみ- 後置演算子は不可
-
演算子の定義は使いたい記号を引用符で囲んで関数定義する
proc `$` (x: myDataType): string = ...
# myDataType型で $ 演算子が使用可能になる
- 演算子は普通の関数のように呼び出し可能
if `==`( `+`(3, 4), 7): echo "True"
前方宣言(Forward Declaration)
- C言語のプロトタイプ宣言のようなもの
# 前方宣言
proc even(n: int): bool
# 相互再起
proc odd(n: int): bool =
assert(n >= 0)
if n == 0: false
else:
n == 1 or even(n-1) # 前方宣言されているのでOK
proc even(n: int): bool =
assert(n >= 0)
if n == 1: false
else:
n == 0 or odd(n-1)
イテレータ
-
iterator
とyeild
キーワードを使う-
proc
と同じくイテレータは多重定義可能
-
iterator countup(a, b: int): int =
var res = a
while res <= b:
yield res
inc(res)
イテレータの制限
- ループでしか呼び出せない
-
return
は使えない
-
- 暗黙的
result
はない - 再起できない
- 前方宣言はできない
- 将来的には前方宣言できるようになる予定
組み込みの基本型
Boolean
-
char
- 常に
1byte
を表す。 -
''
で囲んで表現する -
$
をつけるとstring
に変換できる -
ord
関数で数値化できる (ordinal value
)
- 常に
-
string
- 文字列を表すミュータブルなデータ構造
- ケツに
$0
が付与されるが、Nim
の世界では無視される-
len
では$0
はカウントされないし、アクセスするとエラー -
$0
があるのはcstring
にゼロコピーで変換するため。
-
-
&
演算子で文字列の連結、add
演算子で文字列の追加 - 基本は
UTF-8
エンコーディングだが強制ではない- ファイルから読み込み時は単にバイト列として認識されるので注意
-
integer
int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64
- デフォルトは
int
だが型サフィックス
をつけると指定可能 -
int
はポインタと同じサイズ(処理系依存?)
let
x = 0 # x is of type ``int``
y = 0'i8 # y is of type ``int8``
z = 0'i64 # z is of type ``int64``
u = 0'u # u is of type ``uint``
-
and or xor not
はビット演算子として定義される- 左右のビットシフトはそれぞれ
shl, shr
- 符号なし操作はすべてラップアラウンドする
- オーバーフローまたはアンダーフローエラーを引き起こさない
- コンパイル時にエラーを検出できない場合、型変換によって情報が失われる場合は、
EOutOfRange
例外を投げる
- 左右のビットシフトはそれぞれ
-
float
float float32 float64
- 現状の実装ではデフォルトで
64bit
-
IEEE-754
に従った演算 -
int
からfloat
への変換は明示的に行う必要がある-
toInt
,toFloat
を使う -
int
同様型サフィックス
で指定可能
-
var
x = 0.0 # x is of type ``float``
y = 0.0'f32 # y is of type ``float32``
z = 0.0'f64 # z is of type ``float64``
repr関数
-
$(stringify)
は実装していない型には使えないがrepr
関数なら全部の型に使える-
print
デバッグに便利
-
一歩進んだ型たち
-
type
で型定義
type
biggestInt = int64
biggestFloat = float64 # biggest float type that is available
列挙型
-
enum
キーワードを使う- 内部的には
0, 1, 2...
の整数 -
$
なら定義名、ord
なら内部の整数を返す
- 内部的には
type
Direction = enum
north, east, south, west
var x = south # x は Direction型で値は south
echo x # writes "south" to `stdout`
序数型
- 列挙型、整数型、
char
、bool
、subrange
のことを序数型と呼ぶ - 序数型は特別な操作が用意されている
関数 | コメント |
---|---|
ord(x) | x の内部表現(整数)を返す |
inc(x) | インクリメント |
inc(x, n) | n の分だけ足す |
dec(x) | デクリメント |
dec(x, n) | n の分だけ引く |
succ(x) | returns the successor of x |
succ(x, n) | returns the n'th successor of x |
red(x) | returns the predecessor of x |
red(x, n) | returns the n'th predecessor of x |
subranges
- 表現できる範囲に制限をつけた型
type
MySubrange = range[0..5] # 0 ~ 5 までの整数のみ代入できる型
-
Nim
には自然数
の型がサブレンジで用意されている
type Natural range[0..high(int)]
集合型
- 数学の集合演算を行うための型
- 以下の序数型か内部的にそれに等しい型のみを集合型にできる
int8-int16
uint8/byte-uint16
char
enum
- 内部実装の制限のため
MaxSetElements
は0 .. 2^16
まで-
var s: set[int64]
はエラーになる
-
- リテラルは
{ }
で表現- 空集合は、どんな型も受け入れる状態
type
CharSet = set[char]
var
x: CharSet
x = {'a'..'z', '0'..'9'} # This constructs a set that contains the
# letters from 'a' to 'z' and the digits
# from '0' to '9'
ビットフィールド
- 集合型は関数のフラグ定義に使われることがある
- 整数による定数フラグで
or
演算するよりは型安全になって良い-
chown
の0777
的なやつ
-
# C言語との相互互換を重視するなら cint にする
type
MyFlag* {.size: sizeof(cint).} = enum
A
B
C
D
MyFlags = set[MyFlag]
proc toNum(f: MyFlags): int = cast[cint](f)
proc toFlags(v: int): MyFlags = cast[MyFlags](v)
assert toNum({}) == 0
assert toNum({A}) == 1
assert toNum({D}) == 8
assert toNum({A, C}) == 5
assert toFlags(0) == {}
assert toFlags(7) == {A, B, C}
配列
- 配列は固定長、序数型ならなんでもイデックス可能
- リテラルは
[]
で記述する - 配列のインデックスアクセスのコンパイル時チェックは
--bound_checks:off
オプションで制御可能 -
=
での代入はすべてコピー操作になる。
type
IntArray = array[0..5, int] # 0~5までの整数配列
var
x: IntArray
x = [1, 2, 3, 4, 5, 6]
for i in low(x)..high(x):
echo x[i]
多次元配列
- Nimの多次元配列は次元ごとに異なる型でインデクシングが可能
- よって他の言語と文法が異なる
type
Direction = enum
north, east, south, west
BlinkLights = enum
off, on, slowBlink, mediumBlink, fastBlink
# enum と enum の多次元配列
LevelSetting = array[north..west, BlinkLights]
var
level: LevelSetting
level[north] = on
level[south] = slowBlink
level[east] = fastBlink
echo repr(level) # --> [on, fastBlink, slowBlink, off]
echo low(level) # --> north
echo len(level) # --> 4
echo high(level) # --> west
シーケンス
- 可変長配列で常にヒープに確保されGCの対象になる
- インデクシングは常に
int
で0
から始まる -
@[]
でシーケンスの配列を初期化する - または
newSeq
関数で初期化- なお
@
は配列をシーケンスへ変換する演算子
- なお
- インデクシングは常に
var
x: seq[int] # 整数のシーケンスへの参照
x = @[1, 2, 3, 4, 5, 6] # @を使うと配列をシーケンスにしてヒープへ移動する
# Golang っぽいループとindexが可能
for i, value in @[3, 4, 5]:
echo "index: ", $i, ", value:", $value
Open Array
- 関数のパラメータとしてのみ使用可能
- ただしOpenArrayのネストはできない
- 固定長の配列のみではしばしば不便。
- OpenArrayはIntのみでインデクシング、
0
から始まる
var
fruits: seq[string] # 文字列シーケンス
capitals: array[3, string] # 長さ3の固定長配列
capitals = ["New York", "London", "Berlin"]
fruits.add("Banana")
fruits.add("Mango")
# openArray型のパラメータを受け取る関数
proc openArraySize(oa: openArray[string]): int =
oa.len
assert openArraySize(fruits) == 2 # シーケンス受け取り可能
assert openArraySize(capitals) == 3 # 配列も受取可能
Varargs
- 関数の可変長引数のこと
- 設置できるのは最後のみ
- 関数の実行時には固定長の配列として扱われる
-
varargs
内での型変換も可能
-
proc myWriteln(f: File, a: varargs[string, `$`]) =
for s in items(a):
write(f, s)
write(f, "\n")
myWriteln(stdout, 123, "abc", 4.0)
スライス
- Pythonのスライス操作とほぼ同じ
- 負のIndexを指定するときは
^1
のようにする
- 負のIndexを指定するときは
オブジェクト
- いわゆる構造体とかクラスに近いもの
- 値型なので
=
は全部コピーが走る
- 値型なので
type
Person = object
name: string
age: int
# 自動でコンストラクタ操作可能
# 未指定変数はゼロ値で初期化される
var person1 = Person(name: "Peter", age: 30)
- モジュール外へのオブジェクトの公開は
*
で明示する必要がある。
type
Person* = object # Person型は外部から使用可能
name*: string # 外部モジュールからアクセス可能なメンバ
age*: int
タプル
- オブジェクトと似ているが、構造的な型をもつ点で異なる
- 型の並び厳密にあっている必要があり、同じ並びの型のタプルなら同じ型として扱える特徴がある。
type
Person = tuple
name: string
age: int
# 同じタプル型の別表現
PersonX = tuple[name: string, age: int]
# 匿名表記
PersonY = (string, int)
var
person: Person
personX: PersonX
personY: PersonY
person = (name: "Peter", age: 30)
# Person と PersonX は同じ型として扱える
personX = person
# 匿名タプル表記
personY = ("Peter", 30)
# 同じ型と順番で構成されるので匿名タプルでも互換性あり
person = personY
personY = person
# 簡単な表記
person = ("Peter", 30)
echo person.name # "Peter"
echo person.age # 30
echo person[0] # "Peter"
echo person[1] # 30
# タプル型はどこでも宣言可能
var building: tuple[street: string, number: int]
building = ("Rue del Percebe", 13)
echo building.street
# 以下の代入は型が違うのでだめ!(匿名のPersonYならいけるか?)
#person = building
# --> Error: type mismatch: got (tuple[street: string, number: int])
# but expected 'Person
- 異なるフィールドを持つタプルは型の構造が同じでも別の型として表現される。
- タプルは常にpublicなので外部モジュールからアクセス可能
- タプルは返り値のアンパックにも使えて便利
import os
let
path = "usr/local/nimc.html"
(dir, name, ext) = splitFile(path)
参照とポインタ型
- N対1の関係を作れる
- ポインタ操作。同じメモリ領域をいじる
- なにも参照していない参照型は
nil
-
Nim
は参照を追跡型と非追跡型で明確に区別している- 追跡型の参照点はGC対象のヒープ
-
ref
キーワードで生成
-
- 非追跡型
- ポインタと呼ぶ
-
ptr
キーワードで生成
-
- 非追跡型参照は手動割当のオブジェクトまたはメモリ内の他の場所のオブジェクトを参照する
- GC対象外のなのでメモリリークの可能性ありで
unsafe
- ハードウェアアクセスなどの低レイヤーでは使用必須
- 非追跡のアロケーションは
alloc
,dealloc
,realloc
関数を使う
- ポインタと呼ぶ
- 追跡型の参照点はGC対象のヒープ
-
[]
はderefer
操作で、参照先の値を取得する-
[]
,.
によるメンバアクセスは実はderefer
操作
-
type
Node = ref object
le, ri: Node
data: int
var
n: Node
new(n) # 参照先のアロケーションはnew関数が必須
n.data = 9
# n[].data と書く必要はない / n[].data は非推奨!
手続き型
- いわゆる関数の型
-
nil
も値として許容される
-
- "幾分"抽象的なポインタ型として扱う
- 関数型プログラミングのテクニックを使用するときには必要だろう
proc echoItem(x: int) = echo x
proc forEach(action: proc (x: int)) =
const
data = [2, 3, 5, 7, 11]
for d in items(data):
action(d)
forEach(echoItem)
- 手続き型は、呼び出し規則が型の互換性に影響をあたえることに注意。これは使用上微妙な問題である。
- 呼び出し規則についてはマニュアルを参照のこと
個別型
- 「その型とその基本型の間のサブタイプの関係を暗示しない」という型の作成する機能
-
distinct
キーワードを使う - すべての振る舞いを記述する必要がある。
- 詳しくはマニュアルにて
-
モジュール
- ファイル単位がモジュール
- トップレベルのキーワードかつ
*
がついたものだけエクスポートされる - トップレベルに記述された文はモジュール読み込み時に実行される
-
isMainModule
はmain
としてコンパイルされたらtrue
- 変数は
import
したモジュール間で名前がかぶる場合はmodule.variable
という命名規則で区別する必要がある- 関数の場合、名前が同じでも手続き型が違ければ名前がかぶってもOK
-
import hoge
は全部インポートする-
import mymodule except y
はy
以外を全部インポートする
-
-
from mymodule import x, y, z
形式もあり
from mymodule as m import nil
m.x() # m is aliasing mymodule
include文
- C言語
#include
と同じ -
include fileA, fileB, fileC
と書く- 巨大なモジュールを複数ファイルに分割して書ける