传统上,函数是不透明对象,除了被调用之外,不提供其他功能。(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))))
可以看到,开放式闭包的 keys 和 counter 槽位,能作为局部变量在该开放式闭包的函数体内部访问。同时,我们现在也能从函数体外部访问这些槽位 —— 例如,用于描述一个键盘宏:
(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--keys 和 kbd-macro--counter 是oclosure-define 宏为类型为 kbd-macro 的开放式闭包自动生成的访问器函数。
该宏用于定义一种新的开放式闭包(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该属性用于指定槽位中预期存储的值的类型。
该宏用于创建一个类型为 type 的匿名开放式闭包(OClosure),且该类型必须已通过 oclosure-define 定义完成。slots 应为一个列表,其元素格式为 (slot-name expr)。运行时,每个 expr(表达式)会按顺序求值,随后创建开放式闭包,并将这些求值结果初始化到对应的槽位中。
当该宏创建的开放式闭包以函数形式被调用时(see 调用函数),它会按照 arglist(参数列表)接收参数,并执行 body(函数体)中的代码。body 内可直接引用任意槽位的值,就像引用通过静态作用域捕获的局部变量一样。
若 object 是一个开放式闭包(OClosure),该函数会返回其开放式闭包类型(一个符号);否则返回 nil。
另一个与开放式闭包相关的函数是 oclosure-interactive-form,它允许部分类型的开放式闭包动态计算其交互形式(interactive form)。See oclosure-interactive-form。