Nim 完全指南 / 12 元编程
第 12 章:元编程
元编程是 Nim 最强大的特性之一。Nim 提供三个层次的元编程:模板(Template)、编译期函数(Compile-time procs)、宏(Macro)。
12.1 模板(Template)
模板在调用处展开为代码,避免函数调用开销,类似 C 的宏但类型安全:
12.1.1 基本模板
# 基本模板
template `??`(a, b: untyped): untyped =
if a != nil: a else: b
let name: string = nil ?? "default"
echo name # "default"
# 带参数的模板
template log(msg: string) =
echo "[LOG] ", msg, " (", instantiationInfo().filename, ":", instantiationInfo().line, ")"
log("System started") # [LOG] System started (main.nim:9)
# 模板创建新作用域
template withDir(dir: string, body: untyped) =
let oldDir = getCurrentDir()
setCurrentDir(dir)
try:
body
finally:
setCurrentDir(oldDir)
withDir "/tmp":
echo getCurrentDir() # /tmp
echo getCurrentDir() # 恢复到原目录
12.1.2 模板参数绑定
# 默认绑定规则
template check(cond: untyped, msg: string) =
if not cond:
raise newException(AssertionDefect, msg)
check(1 + 1 == 2, "Math is broken")
# check(1 + 1 == 3, "This will raise")
# 混合绑定
template `!=`(a, b: untyped): untyped {.used.} =
not (a == b)
# untyped 参数:传入未求值的 AST
template repeat(n: int, body: untyped) =
for i in 0..<n:
body
repeat 3:
echo "Hello!"
# 输出三次 "Hello!"
# typed 参数:传入已类型检查的 AST
template doubleIt(x: typed): untyped =
x * 2
echo doubleIt(21) # 42
12.1.3 实用模板
# 计时模板
template timeIt(label: string, body: untyped) =
let start = cpuTime()
body
let elapsed = cpuTime() - start
echo &"{label}: {elapsed * 1000:.2f} ms"
timeIt "排序测试":
var data = newSeq[int](100000)
for i in 0..<data.len:
data[i] = rand(100000)
data.sort()
# 资源管理模板
template withFile(path: string, mode: FileMode, body: untyped) =
var f: File
if open(f, path, mode):
try:
body
finally:
f.close()
else:
raise newException(IOError, "Cannot open: " & path)
withFile("/tmp/test.txt", fmWrite):
f.writeLine("Hello, Template!")
# 断言增强
template assertEq(a, b: untyped) =
let va = a
let vb = b
if va != vb:
raise newException(AssertionDefect,
&"Assertion failed: {va} != {vb}\n" &
&" Left: {repr(va)}\n" &
&" Right: {repr(vb)}")
assertEq(1 + 1, 2)
# assertEq(1 + 1, 3) # 会抛出详细错误信息
12.2 编译期函数
在 const 上下文中执行的函数在编译期运行:
# 编译期纯函数
func fibonacci(n: int): int =
if n <= 1: n
else: fibonacci(n - 1) + fibonacci(n - 2)
# 在编译期计算
const Fib10 = fibonacci(10)
echo Fib10 # 55
# 编译期数组生成
func makeLookupTable(): array[256, int] =
for i in 0..255:
result[i] = i * i
const Squares = makeLookupTable()
echo Squares[10] # 100
# 编译期字符串处理
func reverse(s: string): string =
result = newString(s.len)
for i in 0..<s.len:
result[s.len - 1 - i] = s[i]
const ReversedHello = reverse("Hello")
echo ReversedHello # "olleH"
12.2.1 static 执行
# 使用 static 在编译期执行
const TableSize = static:
var n = 1
while n < 1000:
n *= 2
n
echo TableSize # 1024
# 编译期配置解析
func parseVersion(s: string): (int, int, int) =
let parts = s.split(".")
(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]))
const AppVersion = parseVersion("2.1.3")
echo &"v{AppVersion[0]}.{AppVersion[1]}.{AppVersion[2]}"
12.3 宏(Macro)
宏操作 AST(抽象语法树),是最强大的元编程工具:
12.3.1 基本宏
import std/macros
# dump 打印表达式和结果
macro dump(body: untyped): untyped =
result = quote do:
echo `body`.astrepr, " = ", `body`
dump(1 + 2) # 1 + 2 = 3
dump("hello".len) # "hello".len = 5
12.3.2 AST 操作
import std/macros
# 检查 AST 结构
macro inspect(body: untyped): untyped =
echo body.treeRepr
result = body
inspect:
let x = 10
echo x * 2
# 输出 AST 树形结构:
# StmtList
# LetSection
# IdentDefs
# Ident "x"
# Empty
# IntLit 10
# Call
# Ident "echo"
# Infix
# Ident "*"
# Ident "x"
# IntLit 2
12.3.3 宏生成代码
import std/macros
# 自动生成 getter/setter
macro generateAccessors(typeName: untyped, fields: untyped): untyped =
result = newStmtList()
for field in fields:
let fieldName = field[0]
let fieldType = field[1]
let getterName = ident("get" & ($fieldName).capitalizeAscii)
let setterName = ident("set" & ($fieldName).capitalizeAscii)
result.add quote do:
proc `getterName`(obj: `typeName`): `fieldType` =
obj.`fieldName`
proc `setterName`(obj: var `typeName`, val: `fieldType`) =
obj.`fieldName` = val
type User = object
name: string
age: int
generateAccessors(User, [(name, string), (age, int)])
var u = User(name: "Alice", age: 30)
echo u.getName() # Alice
u.setAge(31)
echo u.getAge() # 31
12.3.4 quote do 语法
import std/macros
# quote do 允许在 AST 中插值
macro makeProc(name: static string, body: untyped): untyped =
let procName = ident(name)
result = quote do:
proc `procName`() =
`body`
makeProc("greet"):
echo "Hello from generated proc!"
greet() # Hello from generated proc!
12.4 实战示例
🏢 场景:自动 JSON 序列化
import std/[macros, strutils]
macro toJson(typeDef: untyped): untyped =
let typeName = typeDef[0][0]
let fields = typeDef[0][2][2]
var toJsonBody = newStmtList()
toJsonBody.add quote do:
result = "{"
for i, field in fields:
let name = field[0]
let nameStr = $name
if i > 0:
toJsonBody.add quote do:
result.add(",")
toJsonBody.add quote do:
result.add("\"" & `nameStr` & "\":")
result.add($obj.`name`)
toJsonBody.add quote do:
result.add("}")
let procName = ident("toJson")
result = quote do:
`typeDef`
proc `procName`(obj: `typeName`): string =
`toJsonBody`
toJson:
type Point = object
x: int
y: int
let p = Point(x: 10, y: 20)
echo p.toJson() # {"x":10,"y":20}
🏢 场景:HTTP 路由注册
import std/macros
macro routes(body: untyped): untyped =
result = newStmtList()
for stmt in body:
if stmt.kind == nnkCall and stmt[0].kind == nnkStrLit:
let path = $stmt[0]
let handler = stmt[1]
echo "Registering route: ", path
result.add quote do:
echo "Route: " & `path`
`handler`
routes:
"/":
echo "Home page"
"/about":
echo "About page"
"/api/users":
echo "Users API"
本章小结
| 特性 | 用途 | 复杂度 |
|---|---|---|
| 模板 | 代码展开、零开销抽象 | 低 |
| 编译期函数 | 编译期计算 | 中 |
| 宏 | AST 操作、代码生成 | 高 |
quote do | AST 引用/插值 | 中 |
static | 强制编译期执行 | 低 |
练习
- 编写一个
debug模板,打印表达式和它的值 - 使用宏自动生成枚举的
$函数 - 实现一个编译期的素数筛
- 创建一个宏,自动为对象生成
==运算符