翼を授ける。
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()