Nim tutorial (II)
Nim Tutorial (Part II)
Author: Andreas Rumpf
はじめに
“繰り返しは、ばかばかしいことを合理的にする” – Norman Wildberger
このドキュメントは、プログラミング言語 Nim の高度な構成についてのチュートリアルです。マニュアルには、より高度な言語機能の例が多数掲載されていますので、この文書は多少古くなっています。
プラグマ
プラグマとは、大量の新しいキーワードを導入することなく、コンパイラに追加の情報やコマンドを与えるための Nim の方法です。プラグマは {.
と .}
という特殊な中括弧で囲まれます。このチュートリアルでは、プラグマは扱いません。利用可能なプラグマについては manual または user guide を参照してください。
オブジェクト指向プログラミング
Nim のオブジェクト指向プログラミング(OOP)に対するサポートは最小限ですが、強力な OOP テクニックを使用することが可能です。OOP はプログラムを設計するための 一つの 方法であり、唯一の 方法ではありません。多くの場合、手続き的なアプローチは、よりシンプルで効率的なコードにつながります。特に、継承よりも合成を優先させた方が良い設計になることが多いです。
継承
Nimでの継承は完全に任意です。実行時の型情報を持つ継承を行うには、オブジェクトは RootObj
から継承する必要があります。これは直接的に行うこともできるし、 RootObj
を継承するオブジェクトを継承することで間接的に行うこともできる。通常、継承された型は ref
型としてマークされますが、これは厳密には強制されません。オブジェクトが特定の型であるかどうかを実行時に確認するために、 of
演算子を使用することができます。
type
Person = ref object of RootObj
name*: string # は `name` が他のモジュールからアクセス可能であることを意味します。
age: int # *がないのは、他のモジュールからフィールドが隠されることを意味します。
Student = ref object of Person # Student は Person を継承しています。
id: int # idフィールドを持つ
var
student: Student
person: Person
assert(student of Student) # is true
# オブジェクトの構築。
student = Student(name: "Anton", age: 5, id: 2)
echo student[]
継承は object of
構文で行われます。多重継承は現在のところサポートされていない。オブジェクトタイプに適切な祖先がいない場合、 RootObj
を祖先として使用することができるが、これは単なる慣習に過ぎない。先祖を持たないオブジェクトは暗黙のうちに final
となります。system.RootObj
とは別に、新しいオブジェクトルートを導入するために、 inheritable
プラグマを使用することができます。(これは例えば GTK ラッパーで使用されています)。
Refオブジェクトは、継承が行われるときには常に使用されるべきです。これは厳密には必要ではありませんが、非Refオブジェクトでは let person.Object
のような代入を行うことができます。Person = Student(id: 123)
のような代入はサブクラスのフィールドを切り詰めます。
注: 単純なコードの再利用のためには、継承(is-a relation)よりも合成(has-a relation)の方が望ましい場合があります。Nim ではオブジェクトは値型なので、合成は継承と同じように効率的です。
相互再帰的な型
オブジェクト、タプル、および参照は、互いに依存し合う非常に複雑なデータ構造をモデル化することができます; これらは 相互に再帰的 です。Nim ではこれらの型は1つの型セクションの中でしか宣言できません。(それ以外の場合、任意のシンボルルルックアヘッドが必要となり、コンパイルが遅くなります)。
例
type
Node = ref object # 以下のフィールドを持つオブジェクトへの参照。
le, ri: Node # leftとrightのサブツリー
sym: ref Sym # 葉には、Symの参照が含まれています。
Sym = object # シンボル
name: string # シンボル名
line: int # シンボルが宣言された行
code: Node # シンボルの抽象構文木
型変換
Nim は型キャストと型変換を区別しています。キャストは cast
演算子を用いて行われ、コンパイラにビットパターンを別の型に解釈するように強制します。
型変換はある型を別の型に変換するための、より丁寧な方法である。型変換は抽象的な 値 を保持しますが、必ずしも ビットパターン を保持するわけではありません。型変換ができない場合、コンパイラは文句を言うか、例外を発生させる。
型変換のシンタックスは destination_type(expression_to_convert)
です(普通の呼び出しと同じです)。
proc getID(x: Person): int =
Student(x).id
x
が Student
でない場合、 InvalidObjectConversionDefect
例外が発生します。
オブジェクトのバリアント
単純なバリアントタイプが必要な状況では、オブジェクト階層が過剰になることがよくあります。
その例です。
# これは、Nimで抽象的な構文木をどのようにモデル化できるかの例です。
type
NodeKind = enum # 異なるノードタイプ
nkInt, # 整数値を持つ葉
nkFloat, # 浮動小数点の葉
nkString, # 文字列の値を持つ葉
nkAdd, # an addition
nkSub, # a subtraction
nkIf # an if statement
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 の値が適合しないため、次のステートメントで `FieldDefect` 例外を発生させます。
n.strVal = ""
この例からわかるように、オブジェクト階層の利点は、異なるオブジェクトタイプ間の変換が不要であることです。しかし、無効なオブジェクトフィールドにアクセスすると、例外が発生します。
メソッド呼び出しの構文
ルーチンを呼び出すための構文シュガーがあります。methodName(obj, args)
の代わりに obj.methodName(args)
という構文が使えます。もし残りの引数がない場合には、括弧を省略することができます: obj.len
(instead of len(obj)
).
このメソッドコールの構文はオブジェクトに限定されるものではなく、どのような型にも使用することができます。
import std/strutils
echo "abc".len # は echo len("abc") と同じです。
echo "abc".toUpperAscii()
echo({'a', 'b', 'c'}.card)
stdout.writeLine("Hallo") # writeLine(stdout, "Hallo")と同じです。
(メソッド呼び出し構文の別の見方として、欠落していたpostfix記法を提供するものである)。
だから、「純粋なオブジェクト指向」のコードは簡単に書けるのです。
import std/[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!")
プロパティ
上記の例からわかるように、Nim には get-properties は必要ありません。メソッド呼び出し構文で呼び出される通常の get-procedureは同じことを実現します。しかし、値の設定は別です。このためには特別なセッター構文が必要です。
type
Socket* = ref object of RootObj
h: int # スターがないため、モジュール外部からはアクセスできません。
proc `host=`*(s: var Socket, value: int) {.inline.} =
## ホストアドレスのsetter
s.h = value
proc host*(s: Socket): int {.inline.} =
## ホストアドレスのgetter
s.h
var s: Socket
new s
s.host = 34 # `host=`(s, 34) と同じです。
(この例では インライン
プロシージャも示しています)。
配列のアクセス演算子 []
はオーバーロードして、 配列のプロパティ
を提供することができる。
type
Vector* = object
x, y, z: float
proc `[]=`* (v: var Vector, i: int, value: float) =
# setter
case i
of 0: v.x = value
of 1: v.y = value
of 2: v.z = value
else: assert(false)
proc `[]`* (v: Vector, i: int): float =
# getter
case i
of 0: result = v.x
of 1: result = v.y
of 2: result = v.z
else: assert(false)
ベクトルはタプルによってよりよくモデル化され、すでに v[]
アクセスを提供しているので、この例は馬鹿げています。
動的なディスパッチ
プロシージャは常に静的ディスパッチを使用します。動的ディスパッチでは、 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.} =
# この基本メソッドをオーバーライドする
quit "to override!"
method eval(e: Literal): int = e.x
method eval(e: PlusExpr): int = eval(e.a) + eval(e.b)
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)))
この例では、コンストラクタ newLit
と newPlus
は静的バインディングを使用するため proc ですが、 eval
は動的バインディングを必要とするため method であることに注意してください。
Note: Nim 0.20 以降、マルチメソッドを使用するには、コンパイル時に明示的に --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
collide(a, b) # output: 2
この例からわかるように、マルチメソッドの呼び出しはあいまいであってはならない。衝突の解決は左から右へ行われるので、衝突2が衝突1より優先される。したがって、 Unit, Thing
は Thing, Unit
よりも優先されます。
パフォーマンスノート: Nim は仮想メソッドテーブルを生成せず、ディスパッチツリーを生成します。これにより、メソッド呼び出しのための高価な間接分岐を回避し、インライン化を可能にします。しかし、コンパイル時の評価やデッドコードの除去など、他の最適化はメソッドでは機能しません。
例外について
Nimでは例外はオブジェクトです。慣習として、例外の型には ‘Error’ というサフィックスが付きます。system モジュールでは例外の階層を定義していますので、それに従った方が良いでしょう。例外は system.Exception
から派生し、共通のインターフェイスを提供します。
例外は、その寿命が不明であるため、ヒープ上に確保する必要があります。コンパイラはスタック上に生成された例外を発生させないようにします。発生したすべての例外は、少なくとも msg
フィールドに発生した理由を指定する必要があります。
慣習として、例外は 例外的 な場合に発生させるべきであり、制御フローの代替方法として使用すべきではありません。
Raiseステートメント
例外を発生させるには raise
ステートメントを使用します。
var
e: ref OSError
new(e)
e.msg = "the request to the OS failed"
raise e
もし raise
キーワードの後に式が続かない場合は、最後の例外が 再レイズ されます。このよくあるコードパターンを繰り返さないようにするために、 system
モジュールの newException
というテンプレートを利用することができます。
raise newException(OSError, "the request to the OS failed")
Tryステートメント
try
文は例外を処理します。
from std/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 OverflowDefect:
echo "overflow!"
except ValueError:
echo "could not convert string to integer"
except IOError:
echo "IO error!"
except:
echo "Unknown exception!"
# 未知の例外を再認識する。
raise
finally:
close(f)
try
の後の文は、例外が発生しない限り実行されます。そして、適切な except
の部分が実行される。
空の except
部分は、明示的にリストアップされていない例外が発生した場合に実行されます。これは if
ステートメントにおける else
パートに似ている。
もし、 finally
パート があれば、それは常に例外ハンドラの後に実行されます。
例外は except
パートの中で 消費 される。例外が処理されない場合、例外はコールスタックを介して伝搬される。つまり,finally
節に含まれない残りの手続きは,(例外が発生しても)実行されないことが多いのです.
もし、except
ブランチの中の実際の例外オブジェクトやメッセージに アクセス する必要がある場合には、system モジュールの getCurrentException() と getCurrentExceptionMsg() プロックを使用するとよいでしょう。
例
try:
doSomethingHere()
except:
let
e = getCurrentException()
msg = getCurrentExceptionMsg()
echo "Got exception ", repr(e), " with message ", msg
例外を発生させたプロックに注釈を付ける
オプションの {.raises.}
プラグマを使うことで、ある proc が特定の例外を発生させるか、あるいはまったく発生させないかを指定することができます。もし {.raises.}
プラグマが使用されると、コンパイラはそれが正しいかどうかを検証します。例えば、ある proc が IOError
を発生させると指定した場合、ある時点でその proc (またはその proc が呼び出すプロック) が新しい例外を発生させ始めたら、コンパイラはその proc をコンパイルできないようにします。
使用例
proc complexProc() {.raises: [IOError, ArithmeticDefect].} =
...
proc simpleProc() {.raises: [].} =
...
このようなコードを作成すると、発生した例外のリストが変更された場合、コンパイラはプラグマの検証を停止した proc の行と発生した例外をキャッチできなかった行、およびキャッチできなかった例外が発生したファイルおよび行を指定するエラーで停止するようになります。これは、変更された問題のあるコードを特定するのに役立つかもしれません。
もし、既存のコードに {.raises.}
プラグマを追加したい場合は、コンパイラが手助けをしてくれます。proc に {.effects.}
プラグマステートメントを追加すると、コンパイラはその時点までに推測されるすべての効果を出力します(例外の追跡は Nim の効果システムの一部です)。このコマンドはモジュール全体のドキュメントを生成し、すべての proc を例外の発生したリストで飾ります。Nim の エフェクトシステムと関連するプラグマについてはマニュアルを参照してください。
ジェネリックス
ジェネリックとは、プロックやイテレータ、型などを type parameters
でパラメタライズするための Nim の手段です。ジェネリックパラメータは角括弧の中に Foo[T]
のように記述されます。このパラメータは型安全なコンテナを効率的に作成するのに役立ちます。
type
BinaryTree*[T] = ref object # BinaryTree は,一般的なパラメータ `T` を持つ
# 汎用型である.
le, ri: BinaryTree[T] # 左右の部分木; nilかもしれません。
data: T # ノードに格納されているデータ
proc newNode*[T](data: T): BinaryTree[T] =
# ノードのコンストラクタ
new(result)
result.data = data
proc add*[T](root: var BinaryTree[T], n: BinaryTree[T]) =
# ツリーにノードを挿入する
if root == nil:
root = n
else:
var it = root
while it != nil:
# データ項目を比較します。`==` と `<` 演算子を持つ任意の型に
# 対して動作する、汎用的な `cmp` プロシージャを使用します。
var c = cmp(it.data, n.data)
if c < 0:
if it.le == nil:
it.le = n
return
it = it.le
else:
if it.ri == nil:
it.ri = n
return
it = it.ri
proc add*[T](root: var BinaryTree[T], data: T) =
# コンビニエンスproc
add(root, newNode(data))
iterator preorder*[T](root: BinaryTree[T]): T =
# 二分木の前置走査。
# これは明示的なスタックを使用します
# (再帰的なイテレータファクトリよりも効率的です)。
var stack: seq[BinaryTree[T]] = @[root]
while stack.len > 0:
var n = stack.pop()
while n != nil:
yield n.data
add(stack, n.ri) # 右のサブツリーをスタックに
n = n.le # プッシュし、左のポインタをたどる
var
root: BinaryTree[string] # BinaryTreeを `string` でインスタンス化する。
add(root, newNode("hello")) # newNode` と `add` をインスタンス化する。
add(root, "world") # 2番目の `add` procをインスタンス化します。
for str in preorder(root):
stdout.writeLine(str)
この例では、一般的なバイナリツリーを示しています。文脈に応じて、括弧は型パラメータを導入するため、またはジェネリックな proc、iterator、または型をインスタンス化するために使われます。この例からわかるように、ジェネリックはオーバーロードと一緒に動作します。シーケンスのための組み込みの add
手続きは隠されておらず、 preorder
イテレータの中で使用されます。
メソッドコールの構文でジェネリックスを使用する場合には、特別な [:T]
構文があります。
proc foo[T](i: T) =
discard
var i: int
# i.foo[int]() # Error: expression 'foo(i)' has no type (or is ambiguous)
i.foo[:int]() # Success
テンプレート
テンプレートはNimの抽象構文木を操作する簡単な置換機構です。テンプレートはコンパイラのセマンティックパスで処理されます。テンプレートは言語の他の部分とうまく統合され、C言語のプリプロセッサ・マクロのような欠点はありません。
テンプレートを呼び出すには、プロシージャのように呼び出します。
例
template `!=` (a, b: untyped): untyped =
# この定義はSystemモジュールに存在する
not (a == b)
assert(5 != 6) # the compiler rewrites that to: assert(not (5 == 6))
演算子 !=
, >
, >=
, in
, notin
, isnot
は、実際にはテンプレートです。これは、 ==
演算子をオーバーロードした場合に、 !=
演算子が自動的に利用でき、正しい処理を行うという利点があります。(IEEE 浮動小数点数の場合は例外です。NaN は基本的なブーリアンロジックを破壊します。)
a > b
は b < a
に変換されます。a in b は
contains(b, a) に変換されます。
notin と
isnot` は明らかな意味を持ちます。
テンプレートは遅延評価のために特に有用です。ロギングのための簡単なプロシージャを考えてみましょう。
const
debug = true
proc log(msg: string) {.inline.} =
if debug: stdout.writeLine(msg)
var
x = 4
log("x has the value: " & $x)
このコードには欠点があります。いつか debug
が false に設定された場合、かなり高価な $
と &
の演算がまだ実行されてしまうのです! (プロシージャの引数評価は eager です)。
log
プロシージャをテンプレート化することで、この問題を解決することができます。
const
debug = true
template log(msg: string) =
if debug: stdout.writeLine(msg)
var
x = 4
log("x has the value: " & $x)
パラメータの型は、通常の型、またはメタ型である untyped
、typed
、type
のいずれかを指定することができます。type は型付きシンボルのみが引数として与えられることを意味し、
untyped` は式がテンプレートに渡される前にシンボルのルックアップや型解決が行われないことを意味します。
テンプレートに戻り値の型が明示されていない場合は、プロックやメソッドとの一貫性を保つために void
が使用されます。
テンプレートにステートメントのブロックを渡すには、最後のパラメータに untyped
を使用します。
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):
txt.writeLine("line 1")
txt.writeLine("line 2")
この例では、2つの writeLine
ステートメントが body
パラメータにバインドされています。withFile
テンプレートは定型的なコードを含んでおり、よくあるバグ、つまりファイルを閉じるのを忘れてしまうことを避けるのに役立っています。let fn = filename
ステートメントが filename
が一度だけ評価されることを保証していることに注意してください。
Example: Lifting Procs
import std/math
template liftScalarProc(fname) =
## スカラーパラメータを受け取りスカラー値を返す proc
## (例 `proc sssss[T](x: T): float`) を持ち上げ,
## seq[T] や入れ子の seq[seq[]] あるいは同じ型の
## シングルパラメータを扱えるテンプレ式の proc を提供します.
##
## .. 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 = typeof(fname(temp))
result = newSeq[outType](x.len)
for i in 0..<x.len:
result[i] = fname(x[i])
liftScalarProc(sqrt) # sqrt() がシーケンスに対して機能するようにする
echo sqrt(@[4.0, 16.0, 25.0, 36.0]) # => @[2.0, 4.0, 5.0, 6.0]
JavaScriptへのコンパイル
Nimのコードは、JavaScriptにコンパイルすることができます。しかし、JavaScript と互換性のあるコードを書くためには、以下のことを覚えなければなりません。
addr
とptr
は JavaScript では意味的に少し異なる。もし、それらがどのようにJavaScriptに変換されるかわからない場合は、それらを避けることが推奨されます。- JavaScript の
cast[T](x)
は(x)
に翻訳されます。ただし、符号付き/符号なし int 間のキャストは例外で、その場合は C 言語の静的キャストと同じように動作します。 - JavaScript における
cstring
は、JavaScript の文字列を意味します。意味的に適切な場合のみcstring
を使用するのがよいでしょう。例えば、バイナリデータのバッファとしてcstring
を使わないでください。
Part 3
次のパートでは、完全にマクロによるメタプログラミングについて説明します。パートIII