템플릿
템플릿 (Templates)
템플릿(template)은 매크로(macro)의 단순한 형태예요. Nim의 추상 구문 트리(abstract syntax tree)에서 동작하는 단순 치환(substitution) 메커니즘이고, 컴파일러의 시맨틱 패스(semantic pass)에서 처리돼요.
템플릿을 호출하는 문법은 프로시저(procedure)를 호출하는 것과 동일해요.
예시를 볼게요.
template `!=` (a, b: untyped): untyped =
# 이 정의는 system 모듈에 존재해요
not (a == b)
assert(5 != 6) # 컴파일러가 이걸 다음처럼 다시 써요: assert(not (5 == 6))
사실 !=, >, >=, in, notin, isnot 연산자는 템플릿이에요.
a > b는b < a로 변환돼요.a in b는contains(b, a)로 변환돼요.notin과isnot은 의미가 명확해요.
템플릿의 "타입"으로는 untyped, typed, typedesc 심볼을 쓸 수 있어요. 이것들은 "메타 타입(meta type)"이라서 특정 문맥에서만 쓸 수 있어요. 일반 타입도 쓸 수 있는데, 이 경우 타입이 정해진(typed) 표현식이 올 것이라고 가정해요.
타입 있는 vs 타입 없는 매개변수 (Typed vs untyped parameters)
untyped 매개변수는 표현식이 템플릿에 전달되기 전에 심볼 탐색(symbol lookup)과 타입 결정(type resolution)을 수행하지 않는다는 뜻이에요. 즉, 예를 들어 아직 선언되지 않은 식별자도 템플릿에 넘길 수 있어요.
template declareInt(x: untyped) =
var x: int
declareInt(x) # 유효함
x = 3
template declareInt(x: typed) =
var x: int
declareInt(x) # 유효하지 않음, x가 선언되지 않아서 타입이 없기 때문
모든 매개변수가 untyped인 템플릿을 즉시 템플릿(immediate template)이라고 불러요. 역사적인 이유로 템플릿에 immediate 프라그마를 명시적으로 붙일 수 있는데, 그런 템플릿은 오버로딩 해석(overloading resolution)에 참여하지 않고 매개변수 타입도 컴파일러가 무시해요. 명시적 즉시 템플릿은 이제 더 이상 권장되지 않아요(deprecated).
참고: 역사적인 이유로
stmt는typed의 별칭이고expr은untyped의 별칭이었지만, 지금은 제거됐어요.
템플릿에 코드 블록 전달하기 (Passing a code block to a template)
템플릿의 마지막 인자로 문장 블록을 특별한 : 구문으로 전달할 수 있어요.
template withFile(f, fn, mode, actions: untyped): untyped =
var f: File
if open(f, fn, mode):
try:
actions
finally:
close(f)
else:
quit("cannot open: " & fn)
withFile(txt, "ttempl3.txt", fmWrite): # 특별한 콜론
txt.writeLine("line 1")
txt.writeLine("line 2")
예시에서 두 writeLine 문장은 actions 매개변수에 묶여요.
보통 템플릿에 코드 블록을 전달하려면, 블록을 받는 매개변수가 untyped 타입이어야 해요. 그러면 심볼 탐색이 템플릿 인스턴스화 시점까지 미뤄지기 때문이에요.
template t(body: typed) =
proc p = echo "hey"
block:
body
t:
p() # 'undeclared identifier: p' 오류 발생
위 코드는 p가 선언되지 않았다는 오류 메시지와 함께 실패해요. 이유는 p() 본문이 body 매개변수에 전달되기 전에 타입 검사(type-check)를 받고, Nim의 타입 검사는 심볼 탐색을 포함하기 때문이에요. 같은 코드는 untyped를 쓰면 동작해요. 전달되는 본문이 타입 검사를 받을 필요가 없기 때문이에요.
template t(body: untyped) =
proc p = echo "hey"
block:
body
t:
p() # 컴파일됨
untyped의 가변 인자 (Varargs of untyped)
타입 검사를 막는 untyped 메타 타입 외에도 varargs[untyped]가 있어요. 이걸 쓰면 매개변수의 개수조차 고정되지 않아요.
template hideIdentifiers(x: varargs[untyped]) = discard
hideIdentifiers(undeclared1, undeclared2)
하지만 템플릿은 varargs를 순회(iterate)할 수 없기 때문에, 이 기능은 일반적으로 매크로에서 훨씬 유용해요.
템플릿에서의 심볼 바인딩 (Symbol binding in templates)
템플릿은 위생적인(hygienic) 매크로라서 새로운 스코프(scope)를 열어요. 대부분의 심볼은 템플릿의 정의 스코프에서 바인딩돼요.
# 모듈 A
var
lastId = 0
template genId*: untyped =
inc(lastId)
lastId
# 모듈 B
import A
echo genId() # 'lastId'가 'genId'의 정의 스코프에서 바인딩됐기 때문에 동작함
제네릭(generic)에서처럼, 심볼 바인딩은 mixin이나 bind 문으로 조절할 수 있어요.
식별자 만들기 (Identifier construction)
템플릿에서는 백틱(backticks) 표기법으로 식별자를 만들 수 있어요.
template typedef(name: untyped, typ: typedesc) =
type
`T name`* {.inject.} = typ
`P name`* {.inject.} = ref `T name`
typedef(myint, int)
var x: PMyInt
예시에서 name은 myint로 인스턴스화되므로 T name은 Tmyint가 돼요.
템플릿 매개변수의 탐색 규칙 (Lookup rules for template parameters)
템플릿의 매개변수 p는 x.p 표현식 안에서도 치환돼요. 그래서 템플릿 인자를 필드 이름으로 쓸 수 있고, 완전 자격(fully qualified)을 갖춘 심볼조차도 같은 인자 이름으로 가려(shadow)질 수 있어요.
# 모듈 'm'
type
Lev = enum
levA, levB
var abclev = levB
template tstLev(abclev: Lev) =
echo abclev, " ", m.abclev
tstLev(levA)
# 결과: 'levA levA'
하지만 전역 심볼은 bind 문으로 제대로 잡을 수 있어요.
# 모듈 'm'
type
Lev = enum
levA, levB
var abclev = levB
template tstLev(abclev: Lev) =
bind m.abclev
echo abclev, " ", m.abclev
tstLev(levA)
# 결과: 'levA levB'
템플릿에서의 위생 (Hygiene in templates)
기본적으로 템플릿은 위생적이에요. 템플릿 안에서 선언한 로컬 식별자는 인스턴스화 문맥에서 접근할 수 없어요.
template newException*(exceptn: typedesc, message: string): untyped =
var
e: ref exceptn # 여기서 e는 암묵적으로 gensym 처리됨
new(e)
e.msg = message
e
# 그래서 이건 동작해요:
let e = "message"
raise newException(IoError, e)
템플릿 안에서 선언된 심볼이 인스턴스화 스코프에 노출될지 말지는 inject와 gensym 프라그마가 결정해요. gensym 처리된 심볼은 노출되지 않고, inject 처리된 심볼은 노출돼요.
엔티티 타입(entity), var, let, const 심볼의 기본값은 gensym이에요. proc, iterator, converter, template, macro의 기본값은 inject인데, 같은 문법 레벨 스코프에 같은 이름의 gensym 심볼이 정의되어 있으면 기본값이 gensym이 돼요. 이건 루틴을 inject로 표시해서 덮어쓸 수 있어요.
엔티티의 이름이 템플릿 매개변수로 전달되면, 그 심볼은 inject 처리된 심볼이 돼요.
template withFile(f, fn, mode: untyped, actions: untyped): untyped =
block:
var f: File # 'f'는 템플릿 매개변수라서 암묵적으로 inject됨
...
withFile(txt, "ttempl3.txt", fmWrite):
txt.writeLine("line 1")
txt.writeLine("line 2")
inject와 gensym 프라그마는 이급(second class) 어노테이션이에요. 템플릿 정의 바깥에서는 의미가 없고, 추상화(abstract)해서 쓸 수도 없어요.
{.pragma myInject: inject.}
template t() =
var x {.myInject.}: int # 동작하지 않음
템플릿의 위생을 없애려면 템플릿에 dirty 프라그마를 쓰면 돼요. dirty 템플릿에서는 inject와 gensym이 아무 효과가 없어요.
gensym 처리된 심볼은 x.field 문법의 필드로 쓸 수 없어요. ObjectConstruction(field: value)나 namedParameterCall(field = value) 같은 문법 구조에서도 쓸 수 없어요.
이유는 다음과 같은 코드가 의도대로 동작해야 하기 때문이에요.
type
T = object
f: int
template tmp(x: T) =
let f = 34
echo x.f, T(f: 4)
하지만 이렇게 하면 메서드 호출(method call) 문법이 gensym 처리된 심볼에는 쓸 수 없게 돼요.
template tmp(x) =
type
T {.gensym.} = int
echo x.T # 유효하지 않음: 대신 'echo T(x)'를 쓰세요.
tmp(12)
메서드 호출 문법의 한계 (Limitations of the method call syntax)
x.f의 x는 f(x)로 다시 써야 한다고 판단하기 전에 시맨틱 검사(즉, 심볼 탐색과 타입 검사)를 거쳐야 해요. 그래서 템플릿/매크로를 호출할 때 점(dot) 문법에는 몇 가지 한계가 있어요.
template declareVar(name: untyped) =
const name {.inject.} = 45
# 컴파일되지 않음:
unknownIdentifier.declareVar
메서드 호출 문법에서 모듈 심볼이 포함된 완전 자격 식별자를 쓰는 것도 불가능해요. 점 연산자가 심볼에 바인딩되는 순서 때문에 그런 건 안 돼요.
import std/sequtils
var myItems = @[1,3,3,7]
let N1 = count(myItems, 3) # OK
let N2 = sequtils.count(myItems, 3) # 완전 자격, OK
let N3 = myItems.count(3) # OK
let N4 = myItems.sequtils.count(3) # 불법, `myItems.sequtils`는 해석할 수 없음
즉, 어떤 이유로 프로시저가 모듈 이름을 통한 구분(disambiguation)이 필요하면, 호출을 함수 호출 문법으로 써야 한다는 뜻이에요.