매크로
매크로
매크로(macro)는 컴파일 타임에 실행되는 특수한 함수입니다. 일반적으로 매크로의 입력은 그 매크로에 전달되는 코드의 추상 구문 트리:idx: (AST:idx:)입니다. 매크로는 그 트리에 변환을 가한 뒤, 변환된 AST를 반환할 수 있습니다. 이는 사용자 정의 언어 기능을 추가하고 도메인 특화 언어:idx: (domain-specific language)를 구현하는 데 사용될 수 있습니다.
매크로 호출은 의미 분석(semantic analysis)이 완전히 위에서 아래로, 왼쪽에서 오른쪽으로 진행되지 않는 사례입니다. 대신 의미 분석은 적어도 두 번 일어납니다:
- 의미 분석이 매크로 호출을 인식하고 해석합니다.
- 컴파일러가 매크로 본문을 실행합니다(그 안에서 다른 프로시저를 호출할 수도 있습니다).
- 매크로 호출의 AST를 매크로가 반환한 AST로 치환합니다.
- 코드의 해당 영역에 대한 의미 분석을 반복합니다.
- 매크로가 반환한 AST에 다른 매크로 호출이 들어 있으면, 이 과정이 반복됩니다.
매크로가 고급 컴파일-타임 코드 변환을 가능하게 하지만, 매크로는 님(Nim)의 문법을 바꿀 수는 없습니다.
스타일 참고: 코드 가독성을 위해, 표현력이 유지되는 가장 약한 프로그래밍 구성을 사용하는 것이 가장 좋습니다. 그래서 "체크 리스트"는 다음과 같습니다:
(1) 가능하면 일반 프로시저/이터레이터를 사용한다. (2) 그렇지 않으면: 가능하면 제네릭 프로시저/이터레이터를 사용한다. (3) 그렇지 않으면: 가능하면 템플릿을 사용한다. (4) 그렇지 않으면: 매크로를 사용한다.
디버그 예제 (Debug example)
다음 예제는 가변 개수의 인자를 받는 강력한 debug 명령을 구현합니다:
# to work with Nim syntax trees, we need an API that is defined in the
# `macros` module:
import std/macros
macro debug(args: varargs[untyped]): untyped =
# `args` is a collection of `NimNode` values that each contain the
# AST for an argument of the macro. A macro always has to
# return a `NimNode`. A node of kind `nnkStmtList` is suitable for
# this use case.
result = nnkStmtList.newTree()
# iterate over any argument that is passed to this macro:
for n in args:
# 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"), newLit(n.repr))
# add a call to the statement list that writes ": "
result.add newCall("write", newIdentNode("stdout"), newLit(": "))
# add a call to the statement list that writes the expressions value:
result.add newCall("writeLine", newIdentNode("stdout"), n)
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)
varargs 파라미터로 전달된 인자는 배열 생성자 표현식으로 감싸집니다. 그래서 debug가 args의 모든 자식을 순회하는 것입니다.
bindSym
위의 debug 매크로는 write, writeLine, stdout이 system 모듈에 선언되어 있어서 인스턴스화하는 문맥에서 보인다는 사실에 의존합니다. 바인딩되지 않은 심볼 대신 바인딩된 심볼(일명 심볼:idx:)을 사용하는 방법이 있습니다. 이를 위해 bindSym 내장 함수를 사용할 수 있습니다:
import std/macros
macro debug(n: varargs[typed]): untyped =
result = newNimNode(nnkStmtList, n)
for x in n:
# we can bind symbols in scope via 'bindSym':
add(result, newCall(bindSym"write", bindSym"stdout", toStrLit(x)))
add(result, newCall(bindSym"write", bindSym"stdout", newStrLitNode(": ")))
add(result, newCall(bindSym"writeLine", bindSym"stdout", x))
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)
이 debug 버전에서는 write, writeLine, stdout 심볼이 이미 바인딩되어 다시 조회되지 않습니다. 예제에서 보듯, bindSym은 오버로드된 심볼과도 암시적으로 동작합니다.
bindSym에 전달되는 심볼 이름은 상수여야 한다는 점에 유의하세요. 실험적 기능인 dynamicBindSym(experimental manual)은 이 값을 동적으로 계산하는 것을 허용합니다.
후행 문 블록 (Post-statement blocks)
매크로는 문(statement) 형태로 호출될 때 인자로 of, elif, else, except, finally, do 블록(루틴 파라미터가 있는 do 같은 다양한 형태를 포함)을 받을 수 있습니다.
macro performWithUndo(task, undo: untyped) = ...
performWithUndo do:
# multiple-line block of code
# to perform the task
do:
# code to undo it
let num = 12
# a single colon may be used if there is no initial block
match (num mod 3, num mod 5):
of (0, 0):
echo "FizzBuzz"
of (0, _):
echo "Fizz"
of (_, 0):
echo "Buzz"
else:
echo num
for 루프 매크로 (For loop macro)
유일한 입력 파라미터로 특수 타입 system.ForLoopStmt의 표현식을 받는 매크로는 for 루프 전체를 다시 쓸 수 있습니다:
import std/macros
macro example(loop: ForLoopStmt) =
result = newTree(nnkForStmt) # Create a new For loop.
result.add loop[^3] # This is "item".
result.add loop[^2][^1] # This is "[1, 2, 3]".
result.add newCall(bindSym"echo", loop[0])
for item in example([1, 2, 3]): discard
다음으로 확장됩니다:
for item in items([1, 2, 3]):
echo item
또 다른 예제:
import std/macros
macro enumerate(x: ForLoopStmt): untyped =
expectKind x, nnkForStmt
# check if the starting count is specified:
var countStart = if x[^2].len == 2: newLit(0) else: x[^2][1]
result = newStmtList()
# we strip off the first for loop variable and use it as an integer counter:
result.add newVarStmt(x[0], countStart)
var body = x[^1]
if body.kind != nnkStmtList:
body = newTree(nnkStmtList, body)
body.add newCall(bindSym"inc", x[0])
var newFor = newTree(nnkForStmt)
for i in 1..x.len-3:
newFor.add x[i]
# transform enumerate(X) to 'X'
newFor.add x[^2][^1]
newFor.add body
result.add newFor
# now wrap the whole macro in a block to create a new scope
result = quote do:
block: `result`
for a, b in enumerate(items([1, 2, 3])):
echo a, " ", b
# without wrapping the macro in a block, we'd need to choose different
# names for `a` and `b` here to avoid redefinition errors
for a, b in enumerate(10, [1, 2, 3, 5]):
echo a, " ", b
case 문 매크로 (Case statement macros)
`case`라는 이름의 매크로는 특정 타입에 대한 case 문 구현을 제공할 수 있습니다. 다음은 튜플에 대한 그러한 구현의 예입니다. 튜플에 대한 기존의 같음 연산자(system.==에 제공되는)를 활용합니다:
import std/macros
macro `case`(n: tuple): untyped =
result = newTree(nnkIfStmt)
let selector = n[0]
for i in 1 ..< n.len:
let it = n[i]
case it.kind
of nnkElse, nnkElifBranch, nnkElifExpr, nnkElseExpr:
result.add it
of nnkOfBranch:
for j in 0..it.len-2:
let cond = newCall("==", selector, it[j])
result.add newTree(nnkElifBranch, cond, it[^1])
else:
error "custom 'case' for tuple cannot handle this node", it
case ("foo", 78)
of ("foo", 78): echo "yes"
of ("bar", 88): echo "no"
else: discard
case 매크로는 오버로드 해석의 대상이 됩니다. case 문의 선택자(selector) 표현식의 타입이 case 매크로의 첫 번째 인자 타입과 대조되어 매칭됩니다. 그런 다음 전체 case 문이 인자 자리에 전달되고 매크로가 평가됩니다.
다시 말해, 매크로는 전체 case 문을 변환해야 하지만, 어떤 매크로를 호출할지 결정하는 데는 문장의 선택자 표현식만 사용됩니다.