NimチュートリアルPart2
NimチュートリアルPart2の個人的な要点まとめ
Ver0.20.0
Pragmas
- コンパイラに付加情報を伝えるための方法
-
{. .}
で囲む - マニュアル
- ユーザーガイド
-
オブジェクト指向
- 継承
- 完全にオプショナルな機能
- 多重継承は現時点では未サポート
- 継承させたいものは
RootObj
を継承している必要がある。-
object of
シンタックス
-
- 継承を伴う型は、厳密には強制されてないが、普通は
ref
としてマークされる。- 実行時にオブジェクトが特定の型であるかどうかを確認するには
of
演算子を使用する
- 実行時にオブジェクトが特定の型であるかどうかを確認するには
- 継承元がない
object
はfinal
扱い
type
Person = ref object of RootObj
name*: string
age: int
Student = ref object of Person # Student は Person を継承
id: int # id フィールドが追加
var
student: Student
person: Person
assert(student of Student) # true
# object の生成
student = Student(name: "Anton", age: 5, id: 2)
echo student[]
- 基本的に継承されるオブジェクトは
ref
だが強制ではない- ただし
non-ref
な型でlet person: Person = Student(id:123)
とかすると、サブクラスフィールドが全部削ぎ落とされる。
- ただし
相互再起型
- 一つの
type
宣言のなかで定義する
type
Node = ref object # a reference to an object with the following field:
le, ri: Node # left and right subtrees
sym: ref Sym # leaves contain a reference to a Sym
Sym = object # a symbol
name: string # the symbol's name
line: int # the line the symbol was declared in
code: Node # the symbol's abstract syntax tree
型変換
- 型キャストと型変換は明確に区別する
- キャストは
cast
演算子を使う - コンパイラは別の型のビットパターンを受け取るようになる
- キャストは
- 型変換はAST的な値を維持したまま変換し内部のbit表現は変化は必須ではない
- 型変換が不可能な場合、コンパイル警告か例外が発生する
InvalidObjectConversionError
- 型変換は
変換したい型名(値)
の形式で行う。
オブジェクトバリアント
- オブジェクトの継承関係よりバリアントがシンプルで良い場合がある
# Nim の抽象構文木をどうモデリングするかの例
type
NodeKind = enum # AST のノードを表す
nkInt, # 整数の AST Node
nkFloat, # Float の AST Node
nkString, # 文字列の AST Node
nkAdd, # 足し算
nkSub, # 引き算
nkIf # if文
Node = ref object
case kind: NodeKind # ``kind`` フィールド は識別者
of nkInt: intVal: int
of nkFloat: floatVal: float
of nkString: strVal: string
of nkAdd, nkSub:
leftOp, rightOp: Node
of nkIf:
condition, thenPart, elsePart: Node
var n = Node(kind: nkFloat, floatVal: 1.0)
# n.kindにfitする値ではないので `FieldError`例外が発生する
n.strVal = ""
- オブジェクト階層の利点
- 異なるオブジェクトタイプ間の変換が必要ない
- 無効なオブジェクトフィールドにアクセスすると例外が発生する
メソッド呼び出しのシンタックス
- メソッド呼び出しには糖衣構文が用意されている
-
method(obj, args)
→obj.method(args)
- 引数なしの場合は
()
省略可能でobj.method
と記述可能 - この糖衣構文はオブジェクト以外にも適用可能
-
import strutils
echo "abc".len # len("abc") と書くのと同じ
echo "abc".toUpperAscii()
echo({'a', 'b', 'c'}.card)
stdout.writeLine("Hallo") # writeLine(stdout, "Hallo") と同じ
- オブジェクト指向のメソッドチェインぽい書き方もできる
import strutils, sequtils
stdout.writeLine("Give a list of numbers (separated by spaces): ")
stdout.write(stdin.readLine.splitWhitespace.map(parseInt).max.`$`)
stdout.writeLine(" is the maximum!")
プロパティ
- メソッド呼び出しの糖衣構文でオブジェクトプロパティの
getter
は必要ないのでは?となるが- しかし
setter
に相当するものはない - よって新しい構文が必要になる
- しかし
type
Socket* = ref object of RootObj
h: int # `*` がついてないので外部に露出していない
proc `host=`*(s: var Socket, value: int) {.inline.} =
## hostアドレスへのsetter
s.h = value
proc host*(s: Socket): int {.inline.} =
## hostアドレスのgetter
s.h
var s: Socket
new s
# 定義した演算子でsetterを実現する
s.host = 34 # same as `host=`(s, 34)
動的ディスパッチ
- プロシージャは常に
static dispatch
- 動的ディスパッチは
proc
の代わりにmethod
キーワードで定義する
type
Expression = ref object of RootObj ## 式のため抽象ベースクラス
Literal = ref object of Expression
x: int
PlusExpr = ref object of Expression
a, b: Expression
# 'eval' は動的ディスパッチバインディングに頼る
method eval(e: Expression): int {.base.} =
# この method をオーバーライドする
quit "to override!"
method eval(e: Literal): int = e.x
method eval(e: PlusExpr): int = eval(e.a) + eval(e.b)
# 以下のコンストラクタは proc で static binding のが理にかなっている
proc newLit(x: int): Literal = Literal(x: x)
proc newPlus(a, b: Expression): PlusExpr = PlusExpr(a: a, b: b)
echo eval(newPlus(newPlus(newLit(1), newLit(2)), newLit(4)))
multi-method
- オブジェクトタイプを持つすべてのパラメーターがディスパッチに使用できる
-
注意:
multi-method
使用時はコンパイラに--multimethods:on
を渡さないとエラー
-
注意:
type
Thing = ref object of RootObj
Unit = ref object of Thing
x: int
method collide(a, b: Thing) {.inline.} =
quit "to override!"
method collide(a: Thing, b: Unit) {.inline.} =
echo "1"
method collide(a: Unit, b: Thing) {.inline.} =
echo "2"
var a, b: Unit
new a
new b
# (unit, unit) の引数なので (Unit, Thing) が呼び出される
collide(a, b) # output: 2
パフォーマンスの注意
- Nimは仮想methodテーブル(いわゆる
vtable
)を生成しないが、メソッドの呼び出しツリーを生成する- メソッド呼び出しの高コストな間接分岐が回避され、インライン化が可能
- しかし
method
はコンパイル時計算やデッドコード除去など、他の最適化はできない
例外処理
- Nimの例外はオブジェクト
-
system.Exception
から派生する - 慣習として
Error
が末尾につく -
msg
フィールドで詳細情報を書くのが必須
-
- 例外は生存期間が不明なので常にヒープ領域に置かれる
- Nimでの慣習として
- 例外は本当に例外的なときだけ発生させるようにしましょう
- 例えばFileが開けないときに例外は起きない
- ファイルがない、というのはよくある一般的な状況で
例外
ではないよね?ということ
- ファイルがない、というのはよくある一般的な状況で
Raise文
-
raise
が単体で使われる場合、前回と同じ例外が投げられる- これを避けるため、
system
の以下のテンプレを使う raise newException(OSError, "the request to the OS failed")
- これを避けるため、
var
e: ref OSError
new(e)
e.msg = "the request to the OS failed"
raise e
Try文
from strutils import parseInt
# ファイルから2行読み込んで足す処理
var
f: File
if open(f, "numbers.txt"):
try:
let a = readLine(f)
let b = readLine(f)
echo "sum: ", parseInt(a) + parseInt(b)
except OverflowError:
echo "overflow!"
except ValueError:
echo "could not convert string to integer"
except IOError:
echo "IO error!"
except:
echo "Unknown exception!"
# reraise the unknown exception:
raise
finally:
close(f)
- 例外オブジェクトは
except
で消費
される-
消費
されなかった場合スタックを通じて例外が伝搬する-
finally
を除く、残りのプロシージャ部分が実行されなくなる
-
-
- 現在の例外オブジェクトにアクセスする
-
system
のgetCurrentException
,getCurrentExceptionMsg
を使う
-
try:
doSomethingHere()
except:
let
e = getCurrentException()
msg = getCurrentExceptionMsg()
echo "Got exception ", repr(e), " with message ", msg
例外でプロシージャにアノテーションを加える
-
{. raises .}
プラグマを使う- このプラグマの指定外の例外が発生するコードがあるとコンパイルが止まる
- 想定外の変更をコンパイラが通知してくれて便利な場合がある
-
{. effects .}
プラグマを追加すると、そのプロシージャの影響(例外含む)を出力してくれるので例外把握に便利。
proc complexProc() {.raises: [IOError, ArithmeticError].} =
...
proc simpleProc() {.raises: [].} =
...
ジェネリクス
- 型安全なコンテナ
type
BinaryTree*[T] = ref object # BinaryTree is a generic type with
# generic param ``T``
le, ri: BinaryTree[T] # left and right subtrees; may be nil
data: T # the data stored in a node
proc newNode*[T](data: T): BinaryTree[T] =
# constructor for a node
new(result)
result.data = data
テンプレート
- NimのASTで動く、かんたんな置換メカニズム
- テキストのプリプロセスに近い。
- Nim本家曰く言語とうまく統合され、Cのプリプロセッサマクロより便利
template `!=` (a, b: untyped): untyped =
# systemモジュールに実際に定義されている例
not (a == b)
assert(5 != 6) # コンパイラが右のように書き換える assert(not (5 == 6))
- Nimの
!=, >, >=, in, notin, isnot
は実際にはテンプレート- 例えば
==
の演算を定義するとテンプレートにより自動的に!=
も使えるようになるという利点がある
- 例えば
テンプレートは遅延評価をしたいときに特に便利
- 以下の例は
debug = false
でもlog()
の呼び出し時の$
などの処理が走ってしまい、無駄な演算コストがかかる
const
debug = true
proc log(msg: string) {.inline.} =
if debug: stdout.writeLine(msg)
var
x = 4
log("x has the value: " & $x)
- これをテンプレート化(上記コードの
proc
をtemplate
に変える)すると無駄な処理を省ける-
log
の部分が愚直にif debug: ..
に置き換わるので、引数の評価が実行されない
-
テンプレートと型
-
untyped
- テンプレートに式が渡される前にシンボル検索と型解決が実行されない
- ブロックをテンプレートに渡したいときは、最後の引数に
untyped
を追加する
typed
-
type
- 型シンボルのみ引数に与えられる
template withFile(f: untyped, filename: string, mode: FileMode,
body: untyped) =
let fn = filename # 引数の評価が一度だけで済むようにするため
var f: File
if open(f, fn, mode):
try:
body
finally:
close(f)
else:
quit("cannot open: " & fn)
withFile(txt, "ttempl3.txt", fmWrite):
# 以下の2秒は body パラメータに渡される
txt.writeLine("line 1")
txt.writeLine("line 2")
- 明示的な返り型のないテンプレートは
void
になる
テンプレートでのプロシージャ上書き
import math
template liftScalarProc(fname) =
## Lift a proc taking one scalar parameter and returning a
## scalar value (eg ``proc sssss[T](x: T): float``),
## to provide templated procs that can handle a single
## parameter of seq[T] or nested seq[seq[]] or the same type
##
## .. code-block:: Nim
## liftScalarProc(abs)
## # now abs(@[@[1,-2], @[-2,-3]]) == @[@[1,2], @[2,3]]
proc fname[T](x: openarray[T]): auto =
var temp: T
type outType = type(fname(temp))
result = newSeq[outType](x.len)
for i in 0..<x.len:
result[i] = fname(x[i])
liftScalarProc(sqrt) # make sqrt() work for sequences
echo sqrt(@[4.0, 16.0, 25.0, 36.0]) # => @[2.0, 4.0, 5.0, 6.0]
JavaScript へのコンパイル
- jsへのコンパイルできるコードを書くうえで気にかけること
-
addr
,ptr
のは JavaScript 上ではセマンティクスが異なる-
js
コンパーチブルにしたければaddr
,ptr
の使用を避けるのが吉 -
js
へのコード変換がどのようにされるか把握しているなら使用OK
-
-
cast[T](x)
はjs
では(x)
にトランケイトされる- 符号付き/符号なしint間のキャストは例外
- C言語では静的キャストとして動作
-
cstring
はjs
の文字列を意味する。-
cstring
はセマンティクス上適切なときのみ使うのが良い慣習 - バイナリデータ用のバッファとかで
cstring
使うのは良くない
-