Nimチュートリアル(パート2)

翻訳者からのメモ
最初の部分は、 「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 = ""
      
      





この例からわかるように、オブジェクト階層とは対照的に、異なるオブジェクトタイプ間で変換を行う必要はありません。 ただし、オブジェクトの間違ったフィールドにアクセスすると例外が発生します。



方法



通常のオブジェクト指向言語では、プロシージャ( メソッドとも呼ばれます)はクラスにバインドされます。 このアプローチには、次の欠点があります。



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つあります。

  1. マクロをプロシージャとして呼び出す(式マクロ)
  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



最後に呼び出します オプションで、問題が発生しているマクロ内の任意の時点で呼び出すことができます。



All Articles