翻訳者からのメモ
最初の部分は、 「Nimチュートリアル(部1)」です。
翻訳は自分のために、つまり不器用にそして急いで行われました。 いくつかのフレーズの言葉遣いはひどい苦痛を生む必要があったため、ロシア語に少しでも似ていました。 より良い書き方を知っている人-個人で書いて、私は編集します。
翻訳は自分のために、つまり不器用にそして急いで行われました。 いくつかのフレーズの言葉遣いはひどい苦痛を生む必要があったため、ロシア語に少しでも似ていました。 より良い書き方を知っている人-個人で書いて、私は編集します。
はじめに
「繰り返しは不条理を慎重に見せる。」-ノーマン・ワイルドバーガー
(原文:「繰り返しはばかげたことを合理的にする。」-ノーマン・ワイルドバーガー)
このドキュメントは複雑なNim言語構成物に関するチュートリアルです。 このドキュメントはやや時代遅れであり、マニュアルには言語の複雑な機能に関するより多くの関連する例があることに注意してください。
プラグマ
プラグマは、新しいキーワードを入力せずにコンパイラに追加情報またはコマンドを伝えるNimの受け入れられた方法です。 プラグマは、ドット
{. and .}
特別な中括弧で囲まれています
{. and .}
{. and .}
。 それらはこのチュートリアルではカバーされていません。 利用可能なプラグマのリストについては、 マニュアルまたはユーザーマニュアルを参照してください。
オブジェクト指向プログラミング
Nimでのオブジェクト指向プログラミング(OOP)のサポートは最小限ですが、強力なOOPテクニックを使用できます。 OOPは、プログラムを開発する方法の1つと見なされますが、それだけではありません。 手続き型アプローチはコードを単純化し、効率を向上させることがあります。 たとえば、継承の代わりに構成を使用すると、多くの場合、アーキテクチャが向上します。
オブジェクト
タプルなどのオブジェクトは、さまざまな値を単一の構造にパックするように設計されています。 しかし、オブジェクトにはタプルにはないいくつかの機能があります:継承と隠蔽情報。 オブジェクトはデータをカプセル化するため、
T()
オブジェクトのコンストラクターは通常、内部開発でのみ使用され、初期化のために、プログラマーは特別な手順( コンストラクターと呼ばれます)を提供する必要があります。
オブジェクトは、実行時にその型にアクセスできます。 の演算子が
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 # : student = Student(name: "Anton", age: 5, id: 2) echo student[]
定義されているモジュールの外部で表示されるオブジェクトのフィールドには、アスタリスク(
*
)が付いています。 タプルとは異なり、さまざまなオブジェクトタイプが同等になることはありません。 新しいオブジェクトタイプは、タイプセクションでのみ定義できます。
継承は
object of
構文の
object of
使用して行われます。 現在、多重継承はサポートされていません。 オブジェクトタイプに適した祖先がない場合は、
RootObj
祖先にすることができますが、これは単なる慣習です。 祖先を持たないオブジェクトは、暗黙的に
final
として宣言されます。
system.RootObj
から継承されない新しいオブジェクトを導入するには、
inheritable
プラグマを使用できます(これは、たとえばGTKラッパーで使用されます)。
参照オブジェクトは、継承に関係なく使用できます。 これは必ずしも必要ではありませんが、たとえば、
let person: Person = Student(id: 123)
非参照オブジェクトが割り当てられている場合、子クラスのフィールドは切り捨てられます。
注:単純なコードの再利用の場合、構成( 「含まれる」関係)は継承( 「ある」関係)よりも望ましい場合があります。 。 Nimのオブジェクトは値型であるため、構成は継承と同じくらい効率的です。
相互再帰型
オブジェクト、タプル、およびリンクを使用して、 相互に依存する相互に再帰的なかなり複雑なデータ構造をモデル化できます。 Nimでは、そのような型は単一の型セクション内でのみ宣言できます。 (他のソリューションでは、追加の文字検索が必要になるため、コンパイルが遅くなります。)
例:
type Node = ref NodeObj # NodeObj NodeObj = object le, ri: Node # sym: ref Sym # , Sym Sym = object # name: string # line: int # , code: PNode #
型変換
Nimは、型キャストと型変換を区別します。 キャストは
cast
演算子を使用して行われ、コンパイラに強制的にバイナリデータを指定された型として解釈させます。
型変換は、ある型を別の型に変換するよりエレガントな方法です。型を変換できるかどうかをチェックします。 型変換が不可能な場合、コンパイラはこれを報告するか、例外がスローされます。
型変換の構文は次のとおりです:
destination_type(expression_to_convert)
(通常の呼び出しに似ています)。
proc getID(x: Person): int = Student(x).id
x
Student
インスタンスで
x
ない場合、
InvalidObjectConversionError
例外がスローされます。
バリアントオブジェクト
オブジェクト階層が過剰な場合があり、単純なバリアント型ですべてを解決できます。
例:
# , # Nim type NodeKind = enum # nkInt, # nkFloat, # nkString, # nkAdd, # nkSub, # nkIf # if Node = ref NodeObj NodeObj = object case kind: NodeKind # ``kind`` of nkInt: intVal: int of nkFloat: floatVal: float of nkString: strVal: string of nkAdd, nkSub: leftOp, rightOp: PNode of nkIf: condition, thenPart, elsePart: PNode var n = PNode(kind: nkFloat, floatVal: 1.0) # `FieldError`, # n.kind : n.strVal = ""
この例からわかるように、オブジェクト階層とは対照的に、異なるオブジェクトタイプ間で変換を行う必要はありません。 ただし、オブジェクトの間違ったフィールドにアクセスすると例外が発生します。
方法
通常のオブジェクト指向言語では、プロシージャ( メソッドとも呼ばれます)はクラスにバインドされます。 このアプローチには、次の欠点があります。
- クラスにメソッドを追加すると、プログラマーはそのメソッドの制御を失うか、クラスとは別にメソッドを操作する必要がある場合は不器用な回避策を講じます。
- 多くの場合、メソッドが何に関連するのかが不明確です
join
は文字列または配列メソッドですか?
Nimは、メソッドをクラスにバインドしないことにより、これらの問題を回避します。 Nimのすべてのメソッドはマルチメソッドです。 後で見るように、マルチメソッドは動的バインディングの手順とのみ異なります。
メソッド呼び出しの構文
Nimでサブルーチンを呼び出すための特別な構文糖衣があります:
obj.method(args)
コンストラクトは
method(obj, args)
と同じ意味です。 引数がない場合は、ブラケットをスキップできます:
len(obj)
代わりに
obj.len
。
このメソッド呼び出し構文はオブジェクトに限定されず、あらゆるタイプに使用できます。
echo("abc".len) # , echo(len("abc")) echo("abc".toUpper()) 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.split.map(parseInt).max.`$`) stdout.writeLine(" is the maximum!")
プロパティ
上記の例からわかるように、Nimはget-propertiesを必要としません。これらは、メソッド呼び出し構文を使用して呼び出される通常のget-proceduresに置き換えられます。 ただし、値の割り当ては別の問題です。このためには、特別な構文が必要です。
type Socket* = ref object of RootObj host: int # , proc `host=`*(s: var Socket, value: int) {.inline.} = ## s.host = value proc host*(s: Socket): int {.inline.} = ## s.host var s: Socket new s s.host = 34 # , `host=`(s, 34)
(この例は
inline
手順も示しています。)
配列プロパティを実装するには、配列アクセス演算子
[]
オーバーロードできます。
type Vector* = object x, y, z: float proc `[]=`* (v: var Vector, i: int, value: float) = # setter case i of 0: vx = value of 1: vy = value of 2: vz = value else: assert(false) proc `[]`* (v: Vector, i: int): float = # getter case i of 0: result = vx of 1: result = vy of 2: result = vz else: assert(false)
この例は不器用です。v
v[]
既にアクセスできるタプルでベクトルをモデル化する方が良いためです。
動的バインディング
プロシージャは常に静的バインディングを使用します。 動的バインディングの場合、
proc
キーワードを
method
置き換えます。
type PExpr = ref object of RootObj ## PLiteral = ref object of PExpr x: int PPlusExpr = ref object of PExpr a, b: PExpr # : 'eval' method eval(e: PExpr): int = # quit "to override!" method eval(e: PLiteral): int = ex method eval(e: PPlusExpr): int = eval(ea) + eval(eb) proc newLit(x: int): PLiteral = PLiteral(x: x) proc newPlus(a, b: PExpr): PPlusExpr = PPlusExpr(a: a, b: b) echo eval(newPlus(newPlus(newLit(1), newLit(2)), newLit(4)))
この例では、
newLit
および
newPlus
はプロシージャであることに注意してください。静的バインディングを使用する方が適切であり、動的バインディングを必要とする
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) # : 2
この例からわかるように、マルチメソッドの呼び出しは曖昧に
collide
ことはできません。解像度が左から右に機能
collide
ため、
collide
1より
collide
2の方が望ましいです。 したがって、
Unit
、
Thing
、
Thing
、
Unit
より
Thing
優先されます。
パフォーマンスに関する注意 :Nimは仮想メソッドのテーブルを作成しませんが、ディスパッチツリーを生成します。 これにより、コストのかかるメソッド呼び出しの間接分岐が回避され、埋め込みが可能になります。 ただし、コンパイル段階での計算やデッドコードの削除など、他の最適化はメソッドでは機能しません。
例外
Nimでは、例外はオブジェクトです。 慣例により、例外タイプは「エラー」で終わります。
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
コマンドは例外を処理します。
# , , # 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
ブランチに似てい
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.}
を使用して、プロシージャが特定の例外を発生させるか、まったく発生させないように指定できます。 プラグマ
{.raises.}
使用されている場合、コンパイラはそれが正しいことを確認します。 たとえば、プロシージャ
IOError
ある時点でそれ(または呼び出されたプロシージャの1つ)が別の例外をスローすることを示す場合、コンパイラはコンパイルを拒否します。 使用例:
proc complexProc() {.raises: [IOError, ArithmeticError].} = ... proc simpleProc() {.raises: [].} = ...
同様のコードを作成した後、スローされた例外のリストが変更されると、コンパイラは、プラグマの検証を停止したプロシージャ内の行とリストにない例外を指すエラーで停止します。 さらに、この例外が発生したファイルと行も示されます。これにより、疑わしいコードを見つけやすくなり、変更が原因となっています。
プラグマ
{.raises.}
を既存のコードに追加する場合は、コンパイラーも役立ちます。 プラグマ
{.effects.}
をプロシージャに追加すると、コンパイラはその時点で表示されるすべての効果を出力します(例外追跡はNim効果システムの一部です)。 プロシージャによってスローされた例外のリストを取得する別の回避策は、Nim
doc2
を使用することです。この
doc2
、モジュール全体のドキュメントを生成し、スローされた例外のリストですべてのプロシージャを装飾します。 エフェクトシステムと関連するプラグマの詳細については、マニュアルを参照してください 。
汎化
汎化は、Nimが型パラメーターを使用してプロシージャ、イテレーター、または型をパラメーター化できるようにするものです。 これらは、高性能のタイプセーフコンテナを作成するのに最も便利です。
type BinaryTreeObj[T] = object # BinaryTree # ``T`` le, ri: BinaryTree[T] # ; nil data: T # BinaryTree*[T] = ref BinaryTreeObj[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] # BinaryTree ``string`` add(root, newNode("hello")) # ``newNode`` add(root, "world") # for str in preorder(root): stdout.writeLine(str)
この例は、一般化されたバイナリツリーを示しています。 コンテキストに応じて、型パラメーターの入力、または一般化されたプロシージャ、反復子、または型のインスタンス化に角括弧が使用されます。 例からわかるように、一般化はオーバーロードで
add
ます。最適な一致は
add
です。 シーケンスの組み込み
add
プロシージャは非表示ではなく、
preorder
反復子で使用されます。
パターン
テンプレートは、Nim抽象構文ツリー(AST)で動作する単純な置換メカニズムです。 テンプレートは、セマンティックコンパイルパスで処理されます。 これらは他の言語とうまく統合されており、Cシャイプリプロセッサマクロの通常の欠点はありません。
テンプレートを呼び出すには、プロシージャとして呼び出します。
例:
template `!=` (a, b: expr): expr = # System not (a == b) assert(5 != 6) # : assert(not (5 == 6))
演算子
!=
、
>
、
>=
、
in
、
isnot
、
isnot
は実際にはテンプレートです:結果として、
==
演算子をオーバーロードすると、
!=
演算子は自動的に使用可能になり、正しく動作します(IEEE浮動小数点数を除く
NaN
は厳密なブール値を破ります)ロジック)。
a > b
は
b < a
a > b
なります。
a in b
contains(b, a)
変換され
contains(b, a)
。
isnot
と
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
に設定されると、かなりコストのかかる操作
$
と
&
が実行されます! (プロシージャの引数の計算は「貪欲」に行われます。)
log
手順をテンプレートに変えると、この問題が解決します。
const debug = true template log(msg: string) = if debug: stdout.writeLine(msg) var x = 4 log("x has the value: " & $x)
パラメータタイプは、通常のタイプまたはメタタイプ
expr
( 式の場合)、
stmt
( コマンドの場合)、または
typedesc
( タイプの説明の場合)です。 戻り値の型がテンプレートで明示的に指定されていない場合、
stmt
プロシージャおよびメソッドとの互換性のために使用されます。
stmt
パラメーターがある場合、それはテンプレート宣言の最後でなければなりません。 理由は、コマンドが特別なコロン構文(
:
を使用してテンプレートに渡されるためです:
template withFile(f: expr, filename: string, mode: FileMode, body: stmt): stmt {.immediate.} = 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
コマンド
writeLine
body
パラメーターにバインドされています。
withFile
テンプレートにはユーティリティコードが含まれており、ファイルを閉じることを忘れないという一般的な問題を回避するのに役立ちます。
let fn = filename
は、
filename
が一度だけ評価されることを保証することに注意してください。
マクロ
マクロを使用すると、コンパイル段階でコードを集中的に変換できますが、Nimの構文を変更することはできません。 ただし、Nim構文は非常に柔軟であるため、これはそれほど深刻な制限ではありません。 マクロは純粋なNimに実装する必要があります。外部関数インターフェイス(FFI)がコンパイラで許可されていないためです。
マクロを記述する方法は2つあります。Nimソースコードを生成し、解析のためにコンパイラに渡すか、コンパイラにフィードされる抽象構文ツリー(AST)を手動で作成します。ASTを構築するには、特定のNim構文が抽象構文ツリーにどのように変換されるかを知る必要があります。ASTはモジュールに文書化されています
macros
。
マクロの準備ができたら、呼び出す方法が2つあります。
- マクロをプロシージャとして呼び出す(式マクロ)
- 特別な構文を使用してマクロを呼び出す
macrostmt
(マクロコマンド)
式マクロ
次の例は、
debug
任意の数の引数を取る強力なコマンドを実装しています。
# Nim API, # ``macros``: import macros macro debug(n: varargs[expr]): stmt = # `n` AST Nim, ; # : result = newNimNode(nnkStmtList, n) # , : for i in 0..n.len-1: # , ; # `toStrLit` AST : result.add(newCall("write", newIdentNode("stdout"), toStrLit(n[i]))) # , ": " result.add(newCall("write", newIdentNode("stdout"), newStrLitNode(": "))) # , : result.add(newCall("writeLine", newIdentNode("stdout"), n[i])) var a: array[0..10, int] x = "some string" a[0] = 42 a[1] = 45 debug(a[0], a[1], x)
マクロ呼び出しは次のように展開されます。
write(stdout, "a[0]") write(stdout, ": ") writeLine(stdout, a[0]) write(stdout, "a[1]") write(stdout, ": ") writeLine(stdout, a[1]) write(stdout, "x") write(stdout, ": ") writeLine(stdout, x)
コマンドマクロ
コマンドマクロは、式マクロと同じ方法で定義されます。ただし、コロンで終わる式を介して呼び出されます。
次の例は、正規表現の字句解析ツールを生成するマクロを示しています。
macro case_token(n: stmt): stmt = # # ... ( -- :-) discard case_token: # , of r"[A-Za-z_]+[A-Za-z_0-9]*": return tkIdentifier of r"0-9+": return tkInteger of r"[\+\-\*\?]+": return tkOperator else: return tkUnknown
最初のマクロを作成する
マクロの作成をガイドするために、典型的な動的コードを静的にコンパイルできるものに変える方法を示します。たとえば、次のコードフラグメントを開始点として使用します。
import strutils, tables proc readCfgAtRuntime(cfgFilename: string): Table[string, string] = let inputString = readFile(cfgFilename) var source = "" result = initTable[string, string]() for line in inputString.splitLines: # if line.len < 1: continue var chunks = split(line, ',') if chunks.len != 2: quit("Input needs comma split values, got: " & line) result[chunks[0]] = chunks[1] if result.len < 1: quit("Input file empty!") let info = readCfgAtRuntime("data.cfg") when isMainModule: echo info["licenseOwner"] echo info["licenseKey"] echo info["version"]
おそらく、このコードフラグメントを商用プログラムで使用して、構成ファイルを読み取り、プログラムの購入者に関する情報を表示できます。この外部ファイルは、ライセンス情報をプログラムに含めるために購入時に生成できます。
version,1.1 licenseOwner,Hyori Lee licenseKey,M1Tl3PjBWO2CC48m
プロシージャ
readCfgAtRuntime
は、指定されたファイル名を開き
Table
、モジュールから戻ります
tables
。
splitLines
モジュールからの手順を使用して、ファイル解析が行われます(エラー処理または境界ケースなし)
strutils
。うまくいかないことがたくさんあります。これはコンパイル時にコードを実行する方法を説明するものであり、コピー防止を適切に実装する方法を説明するものではないことに注意してください。
このコードをコンパイル手順の手順として実装すると、ファイルを取り除くことができます
data.cfg
。そうでなければ、バイナリとともに配布する必要があります。さらに、情報が本当に一定である場合、ロジックの観点からは、情報を可変に保つことは意味がありませんグローバル変数、それが定数であればより良いです。最後に、最も価値のある機能の1つは、コンパイル段階でいくつかのチェックを実装できることです。これは改善された単体テストとして認識できます。これにより、何かが機能しないバイナリを取得することはできません。これにより、1つの小さな重要なファイルの障害のために起動しない壊れたプログラムのユーザーへの配信が防止されます。
ソースコード生成
プログラムを変更して、コンパイル段階で生成されたソースコードを含む行を作成
parseStmt
し、moduleからprocedureに渡すようにします
macros
。以下は、マクロを実装する変更されたソースコードです。
1 import macros, strutils 2 3 macro readCfgAndBuildSource(cfgFilename: string): stmt = 4 let 5 inputString = slurp(cfgFilename.strVal) 6 var 7 source = "" 8 9 for line in inputString.splitLines: 10 # Ignore empty lines 11 if line.len < 1: continue 12 var chunks = split(line, ',') 13 if chunks.len != 2: 14 error("Input needs comma split values, got: " & line) 15 source &= "const cfg" & chunks[0] & "= \"" & chunks[1] & "\"\n" 16 17 if source.len < 1: error("Input file empty!") 18 result = parseStmt(source) 19 20 readCfgAndBuildSource("data.cfg") 21 22 when isMainModule: 23 echo cfglicenseOwner 24 echo cfglicenseKey 25 echo cfgversion
良いことは、ほとんど何も変わっていないことです!まず、入力パラメーターの処理が変更されました(3行目)。動的バージョンでは、プロシージャ
readCfgAtRuntime
は文字列パラメータを受け取ります。ただし、マクロバージョンでは、文字列として宣言されていますが、マクロの外部インターフェイスのみです。マクロが実行されると、実際には
PNimNode
文字列ではなくオブジェクトが取得され、マクロに渡された文字列を取得するために
strVal
モジュールからプロシージャを呼び出す必要があります
macros
(5行目)。
第二に、
readFile
モジュールからプロシージャを使用することはできません
system
- FFI . ( , FFI), , , , . ,
slurp
system
, (
gorge
, ).
,
Table
。代わりに、ソース変数にNimソースコードを生成します。構成ファイルの各行に対して、定数変数が生成されます(15行目)。競合を避けるために、これらの変数のプレフィックスを付けました
cfg
。一般に、コンパイラは、マクロ呼び出し行を次のコードフラグメントに置き換えるだけです。
const cfgversion= "1.1" const cfglicenseOwner= "Hyori Lee" const cfglicenseKey= "M1Tl3PjBWO2CC48m"
これを確認するには、ソースコードの出力を含む行をマクロの最後の画面に追加し、プログラムをコンパイルします。もう1つの違いは、通常のプロシージャ
quit
を呼び出して終了する(呼び出すことができる)代わりに、このバージョンがプロシージャを呼び出すことです
error
(14行目)。手順
error
は同じです
quit
が、さらに、エラーが発生したファイルのソースコードと行番号も表示します。これは、プログラマがコンパイル中にエラーを見つけるのに役立ちます。この状況では、処理中の行ではなく、マクロを呼び出す行を指す
data.cfg
ことになります。これを自分で制御する必要があります。
手動AST生成
ASTを生成するには、理論上、Nimコンパイラが使用する構造を完全に知る必要があります
macros
。これらの構造はmoduleに示されています。一見、これは困難な作業のように見えます。ただし
dumpTree
、式のマクロではなく、コマンドのマクロとして使用することにより、マクロを使用できます。文字の一部を生成したいことがわかっているので
const
、次のソースファイルを作成してコンパイルし、コンパイラが期待するものを確認できます。
import macros dumpTree: const cfgversion: string = "1.1" const cfglicenseOwner= "Hyori Lee" const cfglicenseKey= "M1Tl3PjBWO2CC48m"
ソースコードのコンパイルプロセスでは、次の行の出力が表示されます(これはマクロなので、コンパイルで十分なので、バイナリを実行する必要はありません)。
StmtList ConstSection ConstDef Ident !"cfgversion" Ident !"string" StrLit 1.1 ConstSection ConstDef Ident !"cfglicenseOwner" Empty StrLit Hyori Lee ConstSection ConstDef Ident !"cfglicenseKey" Empty StrLit M1Tl3PjBWO2CC48m
この情報により、コンパイラが必要とするデータをすでによりよく理解できます。コマンドのリストを生成する必要があります。各ソースコード定数について、
ConstSection
およびが生成され
ConstDef
ます。これらすべての定数を単一のブロックに転送した場合、3つの子孫を持つ
const
1つだけが表示され
ConstSection
ます。
気付いていないかもしれませんが、
dumpTree
最初の定数を使用した例では、定数のタイプを明示的に決定しています。これが、最後の2つの定数に出力ツリーに2番目の子が
Empty
あり、最初の定数に文字列識別子がある理由です。したがって、一般に、定義
const
は識別子、オプションのタイプ(空のノードの場合もあります)、および値で構成されます。この知識を武器に、ASTビルドマクロの完成バージョンを見てみましょう。
1 import macros, strutils 2 3 macro readCfgAndBuildAST(cfgFilename: string): stmt = 4 let 5 inputString = slurp(cfgFilename.strVal) 6 7 result = newNimNode(nnkStmtList) 8 for line in inputString.splitLines: 9 # 10 if line.len < 1: continue 11 var chunks = split(line, ',') 12 if chunks.len != 2: 13 error("Input needs comma split values, got: " & line) 14 var 15 section = newNimNode(nnkConstSection) 16 constDef = newNimNode(nnkConstDef) 17 constDef.add(newIdentNode("cfg" & chunks[0])) 18 constDef.add(newEmptyNode()) 19 constDef.add(newStrLitNode(chunks[1])) 20 section.add(constDef) 21 result.add(section) 22 23 if result.len < 1: error("Input file empty!") 24 25 readCfgAndBuildAST("data.cfg") 26 27 when isMainModule: 28 echo cfglicenseOwner 29 echo cfglicenseKey 30 echo cfgversion
ソースコード生成の前の例から始めたので、それとの違いのみに注目します。一時的な型変数を作成し、手動で
string
記述されたようにソースコードを書き込む代わりに、変数を直接使用して、子孫を含むコマンドリストノード()を作成します(行7)。入力行ごとに、定数定義()を作成し、定数セクションでラップします(
result
nnkStmtList
nnkConstDef
nnkConstSection
)これらの変数が作成されると、前のASTツリーダンプに示されているように、階層的にそれらを埋めます(17行目):定数定義はセクション定義の子孫であり、識別子ノード、空のノード(コンパイラーがここにある型を推測できるようにする)および文字列リテラルを含みます値。
マクロ作成の最後のヒント:作成したASTが正常に見えるかどうかわからない場合は、マクロを使用してみてください
dumpTree
。ただし、作成またはデバッグするマクロ内では使用できません。代わりに、生成された行を表示します
treeRepr
。この例の最後に追加すると
echo treeRepr(result)
、マクロを使用したときと同じ出力が表示されます
dumpTree
。最後に呼び出します オプションで、問題が発生しているマクロ内の任意の時点で呼び出すことができます。