强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 doAST 引用/插值
static强制编译期执行

练习

  1. 编写一个 debug 模板,打印表达式和它的值
  2. 使用宏自动生成枚举的 $ 函数
  3. 实现一个编译期的素数筛
  4. 创建一个宏,自动为对象生成 == 运算符

扩展阅读


上一章:泛型编程 | 下一章:错误处理