目次
はじめて
「ループは不条理を合理的にする」– Norman Wildberger
このドキュメントは、Nimプログラミング言語の高レベルの構造です。manualには高級言語機能の例が多く含まれているため、このドキュメントは少し古くなっていることに注意してください。
プラグマ(Pragmas)
プラグマは、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
以外の新しいオブジェクトルートを導入できます。 (例えば、GTKパッケージはこの方法を使用します。)
継承を使用する限り、Refオブジェクトを使用する必要があります。必須ではありませんが、ref以外のオブジェクトの割り当ての場合、例えば、let person:Person = Student(id:123)
はサブクラスフィールドを切り捨てます。
ヒント:単純なコードの再利用の場合、通常、コンポジション(has-a 関係)は継承(is-a関係)よりも優れています。オブジェクトはNimの値型であるため、合成は継承と同じくらい効果的です。
相互再帰型
オブジェクト、タプル、および参照は、相互に依存する非常に複雑なデータ構造をシミュレートできます。これらは相互に再帰的です。Nimでは、これらの型は単一の型セクションでのみ宣言できます。(つまり、任意のシンボルが必要なためにコンパイルが遅くなるその他の型です。)
実例:
type
Node = ref object # 以下のフィールドを持つオブジェクトへの参照:
le, ri: Node # 左右のサブツリー
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
でない場合は、InvalidObjectConversionError
例外が発生されます。
オブジェクトバリアント
単純なバリアント型が必要な場合、通常、オブジェクト階層は過剰です。
実例:
# 以下のは、Nimで抽象構文木をモデル化する方法の実例である
type
NodeKind = enum # さまざまなノード型
nkInt, # 整数値リーフノード
nkFloat, # 浮動小数点リーフノード
nkString, # 文字列リーフノード
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の値が一致しないため、 `FieldError`例外が発生される:
n.strVal = ""
この実例から見ると、オブジェクト階層のメリットは、異なるオブジェクト型の間で変換する必要がないことです。ただし、無効なオブジェクトフィールドにアクセスすると、例外が発生されます。
メソッド呼び出し構文
ルーチンを呼び出すための糖衣構文があります。method(obj,args)
の代わりに構文obj.method(args)
を使用できます。 残りのパラメータがない場合は、括弧を省略できます:obj.len
(l len(obj)
ではない)。
このメソッド呼び出し構文はオブジェクトに限定されず、以下のすべての型に使用できます。
import strutils
echo "abc".len # is the same as echo len("abc")
echo "abc".toUpperAscii()
echo({'a', 'b', 'c'}.card)
stdout.writeLine("Hallo") # the same as 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!")
属性
上記の実例に示されているように、Nimはget-propertiesを必要としません。メソッド呼び出し構文を使用して呼び出される通常のget-proceduresは同じです。ただし、設定値は異なり、特別なsetter文が必要です。
type
Socket* = ref object of RootObj
h: int # アスタリスクがないため、モジュールの外部からアクセスできない
proc `host=`*(s: var Socket, value: int) {.inline.} =
## setter of host address
s.h = value
proc host*(s: Socket): int {.inline.} =
## getter of host address
s.h
var s: Socket
new s
s.host = 34 # same as `host=`(s, 34)
(この実例では、inline
プログラムも示しています。)
[]
配列アクセス演算子をオーバーロードして、配列属性を提供できます。
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 ## abstract base class for an expression
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
は動的バインディングを必要とするためメソッドであることに注意してください。
ヒント: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
実例が示すように、複数のメソッド呼び出しにあいまいがあることは許可しません。解析は左から右であるため、collide2はcollide1より人気です。そのため、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
キーワードの後に式がない場合、最後の例外はre-raisedになります。この一般的なコードパターンの繰り返しを回避するために、system
モジュールでテンプレートnewException
を使用できます。
raise newException(OSError, "the request to the OS failed")
Tryステートメント
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)
例外が発生しない限り、try
後のステートメントが実行されます。次には、適切なexcept
セクションを実行します。
明示的にリストされていない例外がある場合、空のexcept
部分が実行されます。これは、if
文のelse
部分に似ています。
finally
部分がある場合は、常に例外ハンドラの後に実行されます。
except
部分の例外を消費します。例外が処理されない場合、例外はコールスタックによって流れます。これは、プログラムの残りの部分はfinally
節にないことを意味します-通常、実行されません。(例外が発生した場合)
*によってexcept
ブランチの実際の例外オブジェクトまたはメッセージにアクセスする必要がある場合は、systemモジュールのgetCurrentException()またはgetCurrentExceptionMsg()のプロセスを使用できます。例えば:
.. code-block:: nim
try:
doSomethingHere()
except:
let
e = getCurrentException() msg = getCurrentExceptionMsg()
echo "Got exception ", repr(e), " with message ", msg
例外を引き起こすprocsコメント
オプションの {.raises.}
pragmaによって、プロシージャが特定の例外セットを発生させるか、例外がまったく発生しないかを指定できます。{.raises.}
プラグマを使用する場合、コンパイラはこれが真であることを確認します。例えば、指定されたプロシージャがIOError
を発生させ、ある時点でそれ(またはそれが呼び出すプロシージャ)が新しい例外を発生させる場合、コンパイラはプロシージャのコンパイルを阻止します。実例:
proc complexProc() {.raises: [IOError, ArithmeticError].} =
...
proc simpleProc() {.raises: [].} =
...
このようなコードを取得する場合、スローされた例外のリストが変更されると、コンパイラは停止し、プロセスがプラグマの行、キャッチされなかった例外とその行数、およびファイルの検証を停止することを指摘します。キャッチされない例外がスローされています。これは、変更された問題のあるコードを見つけるのに役立つ場合があります。
{.raises.}
プラグマを既存のコードに追加する場合は、コンパイラも役立ちます。 {.effects.}
プラグマステートメントをプロセスに追加すると、コンパイラはその時点までに推測されたすべてのエフェクトを出力します(例外追跡はNimのエフェクトシステムの一部です)。procによって発生した例外のリストを見つけるもう1つの方法は、モジュール全体のドキュメントを生成し、発生した例外のリストですべてのプロシージャを装飾するNim doc2
コマンドを使用することです。Nimのエフェクトシステムと関連するプラグマの詳細については、マニュアルを参照してください。
ジェネリック
ジェネリックスは、型付きパラメータを使用してプロセス、イテレータ、または型をパラメータ化するNimのメリットです。それらは効率的な安全なコンテナに役立ちます:
type
BinaryTree*[T] = ref object # 二分木は、左右のサブツリーのジェネリック型のパラメータ `` T ''であり、nilの可能性がある
le, ri: BinaryTree[T]
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) =
# プロセスを容易にする:
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] # `` string``を使用してバイナリツリーをインスタンス化する
add(root, newNode("hello")) # `` newNode``と `` add``をインスタンス化する
add(root, "world") # 2番目の``add``プロセスをインスタンス化する
for str in preorder(root):
stdout.writeLine(str)
この実例は、一般的な二分木を示しています。コンテキストに応じて、括弧は型パラメータを導入したり、一般的なプロシージャ、イテレータ、または型をインスタンス化するために使用されます。実例が示すように、ジェネリックはオーバーロードを使用します:「add」の最適な一致です。シーケンスの組み込みのadd
プロシージャは非表示ではありませんが、preorder
イテレータで使用されます。
レンプレート
テンプレートは、Nimの抽象構文ツリーで実行できる単純な置換メカニズムです。テンプレートは、コンパイラのセマンティック転送で処理されます。それらは他の言語とうまく統合されており、Cのプリプロセッサマクロの欠陥はありません。
テンプレートを呼び出し、それをプロセスとして使用します。
Example:
template `!=` (a, b: untyped): untyped =
# この定義はsystemモジュールに存在する
not (a == b)
assert(5 != 6) # コンパイラはそれを以下のようにオーバーライドする:assert(not(5 == 6))
!=
, >
, >=
, in
, notin
, isnot
演算子はテンプレートです。これは、自動的に使用できる==
, !=
演算子をオーバーロードするのに適しています。(IEEE浮動小数点数を除く-NaNは基本的なブール論理を破ります。)
a > b
はb < a
に変換されます。a in b
はcontains(b, a)
に変換されます。notin
とisnot
は、名前が示すとおりです。
テンプレートは、計算の遅延に特に役立ちます。簡単なロギングプロセスをご覧ください。
const
debug = true
const
debug = true
proc log(msg: string) {.inline.} =
if debug: stdout.writeLine(msg)
var
x = 4
log("x has the value: " & $x)
このコードには欠点があります。ある日デバッグがfalseに設定されている場合でも、$
および&
操作は実行されます。(プログラムのパラメータ評価は至急です)
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
は、パラメータとして指定できる型シンボルが1つだけであることを意味し、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
が1回だけ評価されることを保証する方法に注意してください。
実例:リフティングプロセス
import math
template liftScalarProc(fname) =
## スカラーパラメータを使用してprocをプロモートし、
## スカラー値を返し、(例えば、 ``proc sssss[T](x: T): float``),
## 単一のseq[T]パラメータまたはネストされたseq[seq[]]または同じ型を処理できるテンプレートプロセスを提供する
##
## .. code-block:: Nim
## liftScalarProc(abs)
## 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) # 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)
に変換されます。この場合、C言語では静的キャストとして表示されます。 cstring
はJavaScriptのJavaScript文字列を表します。cstring
は、意味的に適切な場合にのみ使用することをお勧めします。例えば。cstring
をバイナリデータバッファとして使用しないでください。
コメントを残す