LoginSignup
27
17

More than 1 year has passed since last update.

実践Nimマクロ

Last updated at Posted at 2018-12-14

翼を授ける。

Nimの黒魔術、マクロについての解説を致します。強力過ぎて敬遠されがちなところのあるマクロですが、適切に使えばとても面白い機能です。
この記事では私がNimのマクロを使う中で編み出した使用法を紹介しようと思います。そもそもマクロがよく分からないという方はこちらに良記事があるのでそちらをご参照ください。
Nimで始めるマクロ生活① ~超初級編~

引数の渡され方について

意外とどこにも書いてないのがmacroの引数の渡され方です。引数の型はuntypedで指定していますが、NimNodeKindという型で渡されるようです。引数付きblock系マクロを作る場合、macro側の引数は2つにします。引数なしの場合は1つにします。調査のため以下のコードを書きました。3重のネストまでしか見れませんが、確認用には十分でしょう。

import strformat

macro checkToken(head, body: untyped): untyped =
  for indexI, i in head:
    if i.len() > 1:
        for indexJ, j in i:
          if j.len() > 1:
            for indexK, k in j:
              echo fmt"head[{repr(indexI)}][{repr(indexJ)}][{repr(indexK)}]={repr(k)}"
          else:
            echo fmt"head[{repr(indexI)}][{repr(indexJ)}]={repr(j)}"
    else:
      echo fmt"head[{repr(indexI)}]={repr(i)}"

  for indexI, i in body:
    if i.len() > 1:
        for indexJ, j in i:
          if j.len() > 1:
            for indexK, k in j:
              echo fmt"body[{repr(indexI)}][{repr(indexJ)}][{repr(indexK)}]={repr(k)}"
          else:
            echo fmt"body[{repr(indexI)}][{repr(indexJ)}]={repr(j)}"
    else:
      echo fmt"body[{repr(indexI)}]={repr(i)}"
  echo "\n"

# ----------
checkToken hoge(x, y=2):
    fuga(fugaga(x+y))

checkToken Hoge(Fuga):
    x = 10
    y = 20
    proc test(self: Hoge, x: int, y: int): int =
        return self.x + x + y

コンパイルすると途中で以下のように出力されます。括弧やブロックの中がネストされるようですね。

head[0]=hoge
head[1]=x
head[2][0]=y
head[2][1]=2
body[0][0]=fuga
body[0][1][0]=fugaga
body[0][1][1]=x + y


head[0]=Hoge
head[1]=Fuga
body[0][0]=x
body[0][1]=10
body[1][0]=y
body[1][1]=20
body[2][0]=test
body[2][1]=
body[2][2]=
body[2][3][0]=int
body[2][3][1]=self: Hoge
body[2][3][2]=x: int
body[2][3][3]=y: int
body[2][4]=
body[2][5]=
body[2][6]=
return self.x + x + y

定義の仕方

NimNode定義方式

NimNodeを一つ一つ繋げていく方式です。公式ではこちらが紹介されますが正直書くのが面倒です。

import macros

macro debug(n: varargs[untyped]): typed =
  # `n` is a Nim AST that contains a list of expressions;
  # this macro returns a list of statements (n is passed for proper line
  # information):
  result = newNimNode(nnkStmtList, n)
  # iterate over any argument that is passed to this macro:
  for x in n:
    # add a call to the statement list that writes the expression;
    # `toStrLit` converts an AST to its string representation:
    result.add(newCall("write", newIdentNode("stdout"), toStrLit(x)))
    # add a call to the statement list that writes ": "
    result.add(newCall("write", newIdentNode("stdout"), newStrLitNode(": ")))
    # add a call to the statement list that writes the expressions value:
    result.add(newCall("writeLine", newIdentNode("stdout"), x))

var
  a: array[0..10, int]
  x = "some string"
a[0] = 42
a[1] = 45

debug(a[0], a[1], x)

parseStmt方式(おススメ)

parseStmtはstringをNimNodeに変換する関数です。reprでブロック内をまるごとstringにしてしまえば正規表現などが使えるので、あとはご自由にです。自由度が高いため予想外の挙動を起こす事がありますが、後述するprocを使えばある程度は制御出来ると思います。

import macros
import strformat

macro times(head, body: untyped): untyped =
    var strBody = ""
    for i in body:
        strBody.add(fmt"  {repr(i)}" & "\n")
    parseStmt(fmt"""
for i in 0..{repr(head)}:
{strBody}""")

10.times:
    echo "aaa"
# -> aaa × 10
  • expectLen

親ノードが持つ子ノードの数を明示的に指定します。

  • expectKind

個別の値のノードとしての種別を明示的に指定します。ノードの種別はnimNodeKindで指定します。

import macros
import strformat

dumpTree:
    var x = 1
# ->
# StmtList
#   VarSection
#     IdentDefs
#       Ident "x"
#       Empty
#       IntLit 1

macro varInt(head, body: untyped): untyped =
    body[0].expectKind(nnkIntLit) # int以外は宣言できないようにする
    return newNimNode(nnkStmtList).add(
        newNimNode(nnkVarSection).add(
            newNimNode(nnkIdentDefs).add(
                head,
                newEmptyNode(),
                body[0]
            )
        )
    )
    # parseStmt(fmt"var {repr(head)} = {repr(body[0])}")


varInt x: 1
echo x # -> 1

varInt y: "1" # -> error!

使用例

手前味噌ですがいくつか私の書いたやつで...
これらのマクロは拙作pythonifyに収録されております。(ステマ)

python風defマクロ

proc insertGenerics(args: seq[string]):string =
    var counter = 1
    var ret = ""
    var idx = 0
    var vargs = args[1..^1]
    for i in 'A'..'Z':
        if vargs[idx].find('=') == -1:
            ret.add(i)
        else:
            counter += 1
        if counter >= vargs.len():
            break
        else:
            ret.add(",")
        counter.inc()
        idx.inc()
    return ret

proc insertArgs(args: seq[string]): string =
    var ret = ""
    var gen = 'A'
    var vargs = args[1..^1]
    for i in vargs:
        var gens = ""
        gens.add(gen)
        if i.find('=') != -1:
            ret.add(i & ",")
        else:
            ret.add(i & fmt": {gens},")
        gen.inc()
    ret.delete(ret.len(), ret.len())
    return ret

# ------ def ------
macro def(head, body: untyped): untyped =
    let funcName: string = $head[0]
    var args: seq[string] = @[]
    for i in head:
        args.add(repr(i))
    var bodys: string = repr(body)
    bodys = bodys.replace("\x0A", "\x0A  ")
    var mainNode = ""
    if args.len() == 1:
        mainNode = fmt"proc {args[0]}*() {{.discardable.}} ={bodys}"
    # type anotation
    elif args[0] == "->":
        args[1] = args[1].replace("(", "*(")
        mainNode = fmt"proc {args[1]}: {args[2]} {{.discardable.}} ={bodys}"
    else:
        mainNode = fmt"proc {funcName}*[{insertGenerics(args)}]({insertArgs(args)}): auto {{.discardable.}}={bodys}"
    result = parseStmt(mainNode)


# -----------
def add(x, y):
    return x + y

def typedadd(x: int, y: int) -> int:
    return x + y

echo add(1, 3) # -> 4
echo add(1.0, 2.0) # -> 3.0
echo typedadd(1, 2) # -> 3

python風withマクロ

import macros
import strformat
import strutils
import sequtils

macro with(head, body: untyped): untyped =
    var res = ""
    head.expectLen(3)
    case head.len()
    of 3:
        assert $head[0] == "as"
        var args: seq[string] = @[]
        let classname = repr(head[1][0])
        for i in 1..head[1].len-1:
            args.add(repr(head[1][i]))
        let asname = head[2]
        res = fmt"""
var {asname} = {classname}({args.join(",")})
{asname}.ENTER()
{repr(body)}
{asname}.EXIT()"""
    else:
        discard
    parsestmt(res)

proc ENTER(self: File) {.used.} =
    discard

proc EXIT(self: File) {.used.} =
    self.close

# ----------
with open("hoge.txt", fmRead) as f:
    echo f.readLine()
27
17
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
17