本家チュートリアルのパート2を日本語訳していきます。自分の勉強がてら。
KTakahiro1729さんの書いたPart Ⅰ日本語訳はこちら
- オリジナルの著者:Andreas Rumpf
- nimのバージョン: 0.19.4
はじめに
"繰り返すことは馬鹿げたことを合理的にする" -- ノーマン・ワイルドバーガー
この文書はNimの高度な構文に対するチュートリアルです。この文書はより多くの高度な言語機能の例を含むマニュアルとして使えるものではないことに気をつけてください。
プラグマ(Pragma)
プラグマは多くの新しいキーワードを導入することなくコンパイラに追加情報やコマンドを与えるNimの手法です。プラグマは特別なカッコである{.
と.}
というようなカーリードットブラケットで囲まれて表現されます。 このチュートリアルではプラグマは扱いません。最新のプラグマの詳細はユーザーガイドかマニュアルをご覧ください。
オブジェクト指向プログラミング
Nimのオブジェクト指向プログラミング(OOP)へのサポートは最低限である一方で、強力なOOPのテクニックを使うことができます。 Nimはプログラムを設計する一つの方法であるものの、唯一の方法であるわけではありません。手続き的なアプローチのほうがよりシンプルで、より効率的になることもあるでしょう。例えば継承よりもコンポジションパターンを好むほうがよりよいデザインになることもあるでしょう。
オブジェクト
タプルのように、オブジェクトというものは、構造化された方法で異なる値を一緒にまとめることを意味します。しかしながら、オブジェクトはタプルが持たない多くの機能を提供します:
- 継承と情報の隠蔽を提供します。オブジェクトはデータをカプセル化するので、
T()
というオブジェクトを作る関数は内部的に使われるだけであり、プログラマはオブジェクトを初期化する関数(Proc)を提供するだけで良いのです。 (これをコンストラクタと呼びます。)
オブジェクトは実行時にその型にアクセスできます。オブジェクトの型を確認するのに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) # true
# object 作成:
student = Student(name: "Anton", age: 5, id: 2)
echo student[]
定義したモジュールの外から見れるオブジェクトフィールドは*
でマークされます。タプルとは違って、異なるオブジェクトの型は等価になることはありえません。新しいオブジェクトはtype
セクションの中でだけ定義されます。
継承はオブジェクトのof
構文により成されます。多重継承は現在サポートされていません。もしあるオブジェクトが適切な祖先を持たない場合、RootObj
が祖先となりえますが、これは慣例的なものです。祖先を持たないオブジェクトは最後であることを暗示します。system.RootObj
の代わりに継承可能なプラグマを、新しいオブジェクトを導入する際に使うことができます。(GTKの継承のラッパーに使われています。)
Ref
オブジェクトは継承が使われているときいつでも使えます。厳密には必要ではありませんが、let person: Person = Student(id: 123)
のようなref
のないオブジェクトはサブクラスのフィールドを切り捨てられます。
Note: has-a
の関係を持つコンポジションは同じようにhas-a
の関係を持つ継承よりもシンプルな再利用性のために好まれます。Nimではオブジェクトは値をもつ型なので、コンポジションは継承と同じぐらい効果的なのです。
相互再帰的な型(Mutually recursive type)
オブジェクトやタプルや参照はとても複雑なデータ構造を作ることができ、相互に依存させることができます。相互再帰的な型なのです。Nimではこれらの方は一つのtype
セクション中でのみ宣言されます。 (他の書き方では任意のシンボルの先読み(arbitary symbol lookahead)を必要とし、コンパイルを遅らせることになります。)
例:
type
Node = ref object # 以下のフィールドをもつオブジェクトへの参照:
le, ri: Node # 左右の部分木
sym: ref Sym # 葉に`Sym`への参照を持つ
Sym = object # シンボル
name: string # シンボルの名前
line: int # シンボルが宣言された行
code: Node # シンボルの抽象構文木
型変換
Nimは型のキャストと型の変換を区別します。キャストはキャスト演算子によって成され、コンパイラにビットの配列パターンを他の型であるように強制的に解釈させます。
型変換は型のキャストより丁寧な手法です。型変換では抽象的な値が保持され、ビットのパターンを用いる必要はありません。もし型変換が不可能であった場合、コンパイラはそれを指摘するか、例外を発生させます。
型変換の構文は、目的の型(変換の表現)
(通常の呼び出しのように)です。:
proc getID(x: Person): int =
Student(x).id
xが生徒ではなかったときに InvalidObjectConversionError
例外が発生します。
オブジェクトの階層構造
オブジェクトの階層構造はシンプルなバリアント型が必要な場合に「やり過ぎ」てしまう場合があります。
例:
# これはNimで組まれうる抽象構文木の例です。
type
NodeKind = enum # 異なるノードの型
nkInt, # integerの値をもつ葉
nkFloat, # floatの値をもつ葉
nkString, # stringの値をもつ葉
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)
# 次の文は`FieldError` 例外を発生させます。なぜなら
# n.kindの値が合わないからです。:
n.strVal = ""
例からもわかるように、オブジェクトの階層構造の利点は異なるオブジェクトの型を変換しないでよいということです。一方で、不正なフィールドへのアクセスは例外を発生させます。
メソッド
一般的なオブジェクト指向言語はプロシージャ(メソッドとも言う)はクラスに束縛されています。これは不利な点です。:
プログラマが制御できないクラスにメソッドを追加することは不可能であるか、または醜い回避策が必要です。
メソッドをどこに配置すべきかは明確でないことがよくあります。: join
はstring
メソッドでしょうか、array
のメソッドでしょうか。
Nimはこれらの問題を、クラスにメソッドを割り当てないことで回避しました。
Nimにおけるすべてのメソッドはマルチメソッドです。下記に示すように、マルチメソッドは動的バインディングの目的でのみプロシージャと区別されます。
メソッド呼び出しの構文
ルーチンを呼び出すには糖衣構文があります。: obj.method(args)
という構文はmethod(obj, args)
の代わりに使うことができます。実引数が残らない場合はカッコを省略できます。:len(obj)
の代わりにobj.len
メソッド呼び出し構文はオブジェクトには縛られておらず、どのような型についても使えます。:
import strutils
echo "abc".len # echo len("abc")と同じ
echo "abc".toUpperAscii()
echo({'a', 'b', 'c'}.card)
stdout.writeLine("Hallo") # writeLine(stdout, "Hallo") と同じ
(メソッド呼び出し構文は後置される要素が省略されうというふうにみなすこともできます。)
なので、「完全オブジェクト指向」な書き方は簡単にかけます:
import strutils, sequtils
stdout.writeLine("数字のリストを入力してください。 (スペース区切り): ")
stdout.write(stdin.readLine.splitWhitespace.map(parseInt).max.`$`)
stdout.writeLine(" が最大です")
プロパティ
下の例が示すように、nimはgetプロパティを必要としません。: 通常のget
プロシージャはメソッド呼び出し構文と同じように呼び出せます。しかし、値をセットするのは少し異なります。このため、セッターには特別な構文が必要です。:
type
Socket* = ref object of RootObj
h: int # *がないため、モジュールの外からアクセスすることはできない
proc `host=`*(s: var Socket, value: int) {.inline.} =
## ホストアドレスのセッター
s.h = value
proc host*(s: Socket): int {.inline.} =
## ホストアドレスのゲッター
s.h
var s: Socket
new s
s.host = 34 # same as `host=`(s, 34)
(例はインラインプロシージャでもあります。)
[]
アクセス演算子はarrayプロパティを与えることでオーバーロードできます。:
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)
この例はひどいものです。なぜならvector
はv[]
でアクセスできるタプルでモデル化したほうが良いからです。
動的ディスパッチ
プロシージャはいつも静的に発行されます。
動的に発行するにはproc
をmethod
キーワードに置き換えれば良いです。:
type
Expression = ref object of RootObj ## `expression`のための抽象クラス
Literal = ref object of Expression
x: int
PlusExpr = ref object of Expression
a, b: Expression
# 注意してください: 'eval'は動的バインディングに依存します
method eval(e: Expression): int =
# override this base method
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はメソッドです。
マルチメソッドでは、オブジェクト型を持つすべてのパラメータがディスパッチに使用されます。
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
例が示すように、マルチメソッドの呼び出しはあいまいにすることはできません。Collide 2
はCollide 1
より優先されます。解決は左から右に動作するためです。したがって、Unit、ThingはThing、Unitよりも優先されます。
パフォーマンスの注意点:
Nimは仮想メソッドテーブルを生成しませんが、ディスパッチツリーを生成します。これにより、メソッド呼び出しのための重たい間接分岐が回避され、インライン化が可能になります。ただし、コンパイル時の評価やデッドコードの削除など、他の最適化はメソッドでは機能しません。
例外
Nimでは例外はオブジェクトです。慣例により、例外タイプの末尾には「Error」が付きます。システムモジュールは例外階層を定義します。例外は、共通のインタフェースを提供するsystem.Exceptionから派生します。
例外の寿命は不明なので、例外はヒープに割り当てる必要があります。コンパイラは、スタック上に作成された例外を発生させないようにします。発生したすべての例外は、少なくともmsgフィールドで発生した理由を指定する必要があります。
規約としては、例外的なケースにのみ例外が発生されるべきということがあります。たとえば、ファイルを開くことができない場合、これは非常に一般的であるため、例外を発生させることはできません(ファイルが存在しない場合があります)。
Raise statement
例外の発生は、raise
文により成されます。:
var
e: ref OSError
new(e)
e.msg = "the request to the OS failed"
raise e
raiseキーワードの後に式が続かない場合は、最後の例外が再度発生します。この共通コードパターンの繰り返しを避けるために、システムモジュール内のテンプレートnewException
を使用することができます。
raise newException(OSError, "the request to the OS failed")
Try statement
try
文で例外を扱います。:
from strutils import parseInt
# 数値を含んでいると想定されるテキストファイルの最初の二行を読み込む
# そしてそれらを足そうとする
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)
try
以下の文は例外が発生しない場合にのみ実行されます。そして適切な例外が実行されます。
明示的にリストされていない例外がある場合は、空のexcept
部分が実行されます。 if文のelse部分と似ています。
finally部分がある場合は、常に例外ハンドラの後に実行されます。
例外はexcept
部分で消費されます。例外が処理されない場合は、コールスタックを通じて伝播されます。これはつまり、(例外が発生した場合)プロシージャの残りの部分(finally節内にない)が実行されないことを意味します。
except
ブランチ内の実際の例外オブジェクトまたはメッセージにアクセスする必要がある場合は、システムモジュールからgetCurrentException()
およびgetCurrentExceptionMsg()
プロシージャを使用できます。例:
try:
doSomethingHere()
except:
let
e = getCurrentException()
msg = getCurrentExceptionMsg()
echo "Got exception ", repr(e), " with message ", msg
発生する例外の注釈をprocsに付ける (Annotating procs with raised exceptions)
オプションの{.raises.}
プラグマを使用することで、proc
が特定の例外を発生させること、またはまったく発生させないことを指定することができます。 {.raises.}
プラグマが使用されている場合、コンパイラはこれが正しいことを確認します。たとえば、あるprocがIOError
を発生させるように指定した場合、ある時点で(あるいはそれが呼び出すprocの1つが)新しい例外を発生させ始めると、コンパイラはそのprocがコンパイルできないようにします。使用例:
proc complexProc() {.raises: [IOError, ArithmeticError].} =
...
proc simpleProc() {.raises: [].} =
...
このようなコードを書けば、発生した例外のリストが変更された場合、コンパイラは次のようなエラーを出して停止します。
- プラグマの検証を停止した
proc
の行 - 発生したが補足されなかった例外
- およびその例外をもつファイルと行
これらは変更された問題のあるコードを見つけるのに役立ちます。
既存のコードに{.raises.}
プラグマを追加したい場合は、コンパイラが役に立ちます。
{.effects.}
プラグマステートメントをproc
に追加すると、コンパイラはそれまでに推定されたすべてのeffect
を出力します(例外追跡はNimのeffect
システムの一部です)。
procによって発生した例外のリストを見つけるためのもう1つのより間接的な方法は、モジュール全体のドキュメントを生成し、発生した例外のリストですべてのprocを装飾するNim doc2コマンドを使用することです。マニュアルでNimのエフェクトシステムと関連プラグマについてもっと読むことができます。
ジェネリクス(Generics)
Generics are Nim's means to parametrize procs, iterators or types with type parameters. They are most useful for efficient type safe containers:
ジェネリクスは、型パラメータを使用してproc、イテレータ、または型をパラメータ化するためのNimの手段です。それらは便利な型安全なコンテナに最も有用です:
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
proc add*[T](root: var BinaryTree[T], n: BinaryTree[T]) =
# insert a node into the tree
if root == nil:
root = n
else:
var it = root
while it != nil:
# compare the data items; uses the generic ``cmp`` proc
# that works for any type that has a ``==`` and ``<`` operator
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) =
# convenience proc:
add(root, newNode(data))
iterator preorder*[T](root: BinaryTree[T]): T =
# Preorder traversal of a binary tree.
# Since recursive iterators are not yet implemented,
# this uses an explicit stack (which is more efficient anyway):
var stack: seq[BinaryTree[T]] = @[root]
while stack.len > 0:
var n = stack.pop()
while n != nil:
yield n.data
add(stack, n.ri) # push right subtree onto the stack
n = n.le # and follow the left pointer
var
root: BinaryTree[string] # instantiate a BinaryTree with ``string``
add(root, newNode("hello")) # instantiates ``newNode`` and ``add``
add(root, "world") # instantiates the second ``add`` proc
for str in preorder(root):
stdout.writeLine(str)
例は一般的な二分木を示しています。文脈に応じて、角括弧[ ]
は、型パラメータを導入するため、または一般的なproc
、iterator
、またはtype
をインスタンス化するために使用されます。例が示すように、ジェネリックスはオーバーロードを伴って動作します: 最もよくマッチしたadd
が使われるわけです。シーケンスの組み込み追加手続きは隠されておらず、preorderイテレータで使用されます。
テンプレート
テンプレートはNimの抽象構文木を扱うシンプルな代替メカニズムです。テンプレートはコンパイラのセマンティック処理中に処理されます。テンプレートによって言語は拡張され、Cのプリプロセッサの欠点は共有されないようになっています。
テンプレートを実行するにはプロシージャを呼ぶように使います。
例:
template `!=` (a, b: untyped): untyped =
# この定義はSystem modulesに存在する
not (a == b)
assert(5 != 6) # コンパイラはこれをこのように書き換える: assert(not (5 == 6))
!=
,>
,>=
, in
, notin
, isnot
といった演算子は実際にテンプレートで実装されています。: これはもし、==演算子をオーバーロードしたときには!=演算子自動的に使えるようになり、正しく動くようになるというメリットがあります。(IEEEの浮動小数点数 - NaNだけ例外で通常のbooleanの挙動にはなりません。)
a > b
はb < a
に、a in b
はcontains(b, a)
に変換されます。notin
and isnot
は明確な意味を持ちます。
テンプレートは特に遅延評価の目的において便利です。ログのための簡単なプロシージャを例に考えてみましょう。:
const
debug = true
proc log(msg: string) {.inline.} =
if debug: stdout.writeLine(msg)
var
x = 4
log("x は次の値です: " & $x)
このコードは欠点があります。: もしdebug
がfalse
になっても、計算量の大きな演算子である$
と&
演算子は動き続けるのです! (実引数への評価は先行評価なのです).
ログのプロシージャをテンプレートに変えることでこの問題は解決されます。:
const
debug = true
template log(msg: string) =
if debug: stdout.writeLine(msg)
var
x = 4
log("x は次の値です: " & $x)
仮引数の方はordinary型か、meta型であるuntype
かtyped
かtype
です。 型推論は実引数より与えられる型のシンボルで行われ、untyped
はシンボル探索と型分析がテンプレートに式がわたる前に実行されていないということを締めします。
もしテンプレートが曖昧さのない型を返すのであれば、プロシージャやメソッドとの一貫性のためにvoid
が使われます。
テンプレートのステートメントのブロックを通すには、最後の仮引数にuntyped
を使ってください:
template withFile(f: untyped, filename: string, mode: FileMode,
body: untyped): typed =
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
文がファイル名を一度だけ確かめている方法に注意してみてください。.
例: リフティングプロシージャ
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へのコンパイル
NimはJavaScriptにコンパイルできます。しかしながらJavaScriptに適合したコードを書くには以下のことに気をつけてください:
-
addr
とptr
はJSにおいて少し異なる意味を持ちます。もしJSにどのように翻訳されるのか、詳しくない場合にはこれらのキーワードを避けたほうが望ましいでしょう。 -
cast[T](x)
はJSでは(x)
と変換されますが、unsigned int型とsigned int型の間の変換はC言語における静的なキャストと同じように振る舞います。 -
cstring
はJSではstring
を意味します。意味論的に適切な場合にのみcstringを用いることが良いプラクティスとなります。 例えば、バイナリのバッファとしてcstring
を使ってはいけません。
Part 3
次のパートではマクロを使ったメタプログラミングについて扱います: Part III
訳注
正直全然わからない文がかなり存在していたので、フィーリングで書いてある部分が多いです。
色々間違ってると思うのですがご指摘ください。
Nimの学習を続けていくうちにあーこれは誤訳だなと思われる箇所を見つけたら逐次修正していきます。
TODO
- 本家ドキュメントへのリンクを追記