13.11 开放式闭包

传统上,函数是不透明对象,除了被调用之外,不提供其他功能。(Emacs Lisp 函数并非完全不透明,因为你可以从中提取一些信息,例如文档字符串、参数列表或交互规范,但它们在很大程度上仍然是不透明的。)这通常是我们所需要的,但偶尔我们需要函数对外暴露更多关于自身的信息。

开放式闭包(Open closures),简称 OClosures,是一类携带额外类型信息、并以槽位(slots)形式暴露自身部分信息的函数对象,你可以通过访问器函数来读取这些槽位。

开放式闭包的定义分为两步:首先使用 oclosure-define,通过指定该类型开放式闭包所包含的槽位,来定义一种新的 OClosure 类型;然后使用 oclosure-lambda 创建指定类型的 OClosure 对象。

假设我们想要定义键盘宏 —— 即能重新执行一系列按键事件的交互式函数(see Keyboard Macros)。你可以通过普通函数实现,示例如下:

(defun kbd-macro (key-sequence)
  (lambda (&optional arg)
    (interactive "P")
    (execute-kbd-macro key-sequence arg)))

但通过这种方式定义的函数,你无法便捷地从中提取出 key-sequence(按键序列)—— 比如要打印这个序列时就会遇到困难。

我们可以通过开放式闭包解决这个问题,具体步骤如下。首先定义我们的键盘宏类型(同时我们决定为该类型新增一个 counter 槽位):

(oclosure-define kbd-macro
  "Keyboard macro."
  keys (counter :mutable t))

完成类型定义后,我们即可重写 kbd-macro 函数:

(defun kbd-macro (key-sequence)
  (oclosure-lambda (kbd-macro (keys key-sequence) (counter 0))
      (&optional arg)
    (interactive "P")
    (execute-kbd-macro keys arg)
    (setq counter (1+ counter))))

可以看到,开放式闭包的 keyscounter 槽位,能作为局部变量在该开放式闭包的函数体内部访问。同时,我们现在也能从函数体外部访问这些槽位 —— 例如,用于描述一个键盘宏:

(defun describe-kbd-macro (km)
  (if (not (eq 'kbd-macro (oclosure-type km)))
      (message "Not a keyboard macro")
    (let ((keys    (kbd-macro--keys km))
          (counter (kbd-macro--counter km)))
      (message "Keys=%S, called %d times" keys counter))))

其中 kbd-macro--keyskbd-macro--counteroclosure-define 宏为类型为 kbd-macro 的开放式闭包自动生成的访问器函数。

Macro: oclosure-define oname &optional docstring &rest slots

该宏用于定义一种新的开放式闭包(OClosure)类型,同时为其slots(槽位)生成对应的访问器函数。oname 可以是一个符号(即该新类型的名称),也可以是形如 (oname . type-props) 的列表 —— 这种情况下,type-props 是该开放式闭包类型的额外属性列表。slots 是一组槽位描述的列表,其中每个槽位可以是一个符号(即槽位名称),也可以是形如 (slot-name . slot-props) 的结构,其中 slot-props 是对应槽位 slot-name 的属性列表。 由 type-props 指定的开放式闭包类型属性可包含以下内容:

(:predicate pred-name)

该属性要求创建一个名为 pred-name 的断言函数(predicate function)。此函数将用于识别类型为 oname 的开放式闭包(OClosure)。若未指定该类型属性,oclosure-define 会为这个断言函数生成一个默认名称。

(:parent otype)

该属性将开放式闭包类型 otype 设置为类型 oname 的父类型。类型为 oname 的开放式闭包会继承其父类型定义的所有 slots(槽位)。

(:copier copier-name copier-args)

该属性会触发一个「函数式更新函数」的定义(这类函数也被称为 复制器(copier))。该函数接收一个类型为 oname 的开放式闭包作为第一个参数,返回该闭包的副本 —— 副本中名为 copier-args 的槽位会被修改,取值为调用 copier-name 时传入的对应参数值。

对于 slots 中的每一个槽位,oclosure-define 宏都会创建一个名为 oname--slot-name 的访问器函数;这些函数可用于读取对应槽位的值。slots 中的槽位定义可指定该槽位的以下属性:

:mutable val

默认情况下,槽位是不可变的;但如果为 :mutable 属性指定非 nil 的值,该槽位将变为可修改状态,例如可通过 setf 函数修改(see setf)。

:type val-type

该属性用于指定槽位中预期存储的值的类型。

Macro: oclosure-lambda (type . slots) arglist &rest body

该宏用于创建一个类型为 type 的匿名开放式闭包(OClosure),且该类型必须已通过 oclosure-define 定义完成。slots 应为一个列表,其元素格式为 (slot-name expr)。运行时,每个 expr(表达式)会按顺序求值,随后创建开放式闭包,并将这些求值结果初始化到对应的槽位中。

当该宏创建的开放式闭包以函数形式被调用时(see 调用函数),它会按照 arglist(参数列表)接收参数,并执行 body(函数体)中的代码。body 内可直接引用任意槽位的值,就像引用通过静态作用域捕获的局部变量一样。

Function: oclosure-type object

object 是一个开放式闭包(OClosure),该函数会返回其开放式闭包类型(一个符号);否则返回 nil

另一个与开放式闭包相关的函数是 oclosure-interactive-form,它允许部分类型的开放式闭包动态计算其交互形式(interactive form)。See oclosure-interactive-form


emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike