表达式的实现

针对有兴趣自行实现部分或完整内核的读者,这里有几点值得注意:

存储数据

表达式需要内联存储某些数据或缓存起来,以避免代价过高的重复计算。例如,创建一个表达式 app x y 时,需要计算并存储该应用表达式的哈希摘要(hash digest)。这一步应利用已缓存的 xy 的哈希值,而非递归遍历整个 x 树来计算,再对 y 做同样操作。

通常你会想内联存储的数据包括:

  • 哈希摘要
  • 表达式中自由绑定变量(loose bound variables)的数量
  • 表达式是否包含自由变量(has free variables)

后两者分别对实例化(instantiation)和抽象化(abstraction)优化非常有用。

举个“智能构造函数”示例,用于构造 app 表达式:

def mkApp x y:
  let hash := hash x.storedHash y.storedHash
  let numLooseBvars := max x.numLooseBvars y.numLooseBvars
  let hasFvars := x.hasFvars || y.hasFvars
  .app x y (cachedData := hash numLooseBVars hasFVars)

禁止深拷贝

表达式的实现应确保用于构造父表达式的子表达式不会被深拷贝。换句话说,创建 app x y 表达式时,不应递归复制 xy 的所有元素,而应采用某种引用机制,如指针、整数索引、垃圾回收对象的引用、引用计数对象等(任意一种方案都能提供合理性能)。

如果你的默认表达式构造策略是深拷贝,将无法在内存允许范围内构造任何非平凡的环境。

自由绑定变量数量的示例实现

numLooseBVars e:
    match e with
    | Sort | Const | FVar | StringLit | NatLit => 0
    | Var dbjIdx => dbjIdx + 1,
    | App fun arg => max fun.numLooseBvars arg.numLooseBvars
    | Pi binder body | Lambda binder body => 
    |   max binder.numLooseBVars (body.numLooseBVars - 1)
    | Let binder val body =>
    |   max (max binder.numLooseBvars val.numLooseBvars) (body.numLooseBvars - 1)
    | Proj _ _ structure => structure.numLooseBvars

对于 Var 表达式,自由绑定变量的数量是其 deBruijn 索引加一。原因在于,我们计算的是该变量不再“松弛”(loose)所需包裹的绑定器数量,其中 +1 是因为 deBruijn 索引是从 0 开始计数的。

例如:

  • 对于表达式 Var(0),需要在该绑定变量之上包裹 1 个绑定器,变量才不再松弛。
  • 对于表达式 Var(3),需要包裹 4 个绑定器:
--  索引:3 2 1 0
fun a b c d => Var(3)

当我们创建一个新的绑定器(lambda、pi 或 let)时,可以对主体中的自由绑定变量数量执行饱和减 1(即自然数减法,但不会小于 0),因为主体相当于多包裹了一个绑定器。