13.8 泛型函数

使用 defun 定义的函数,对其参数的类型和预期取值有着固定的预设。例如,一个设计用来处理数字或数字列表的函数,如果传入其他类型的值(如向量或字符串),就会运行失败或抛出错误。这是因为函数的实现并没有准备好处理设计时预设之外的类型。

与之相对,面向对象程序使用 多态函数(polymorphic functions):一组同名的专用函数,每一个都针对某一组特定的参数类型编写。实际调用哪一个函数,会在运行时根据实际参数的类型来决定。

Emacs 提供了对多态的支持。与其他 Lisp 环境(尤其是 Common Lisp 及其公共 Lisp 对象系统 CLOS)类似,这一支持基于 泛型函数(generic functions)。Emacs 的泛型函数高度遵循 CLOS 规范,包括使用相似的命名。因此,如果你有 CLOS 使用经验,本节后续内容会让你感到非常熟悉。

泛型函数通过定义其名称与参数列表来指定一个抽象操作,但(通常)不提供实现。针对若干特定类参数的实际实现由 方法(methods) 提供,这些方法需要单独定义。实现某个泛型函数的每个方法都与该泛型函数同名,但方法的定义会通过对泛型函数所定义的参数进行 特化(specializing),来表明它可以处理哪些类型的参数。这些 参数特化符(argument specializers) 可以更具体或更一般;例如,string 类型就比更一般的类型(如 sequence)更加具体。

注意,与基于消息的面向对象语言(如 C++ 和 Simula)不同,实现泛型函数的方法并不属于某个类,而是属于它们所实现的泛型函数。

当调用一个泛型函数时,它会将调用者传入的实际参数与每个方法的参数特化符进行比较,从而选出适用的方法。如果调用的实际参数与某个方法的特化符兼容,则该方法是适用的。如果有多个方法适用,则会按照后面描述的特定规则将它们组合起来,由组合后的结果处理此次调用。

Macro: cl-defgeneric name arguments [documentation] [options-and-methods…] &rest body

该宏用于定义一个泛型函数,指定其 name(名称)和 arguments(参数列表)。若提供了 body(函数体),则该部分会作为泛型函数的默认实现;若提供了 documentation(文档说明,建议始终提供),则需以 (:documentation docstring) 的形式指定泛型函数的文档字符串。可选的 options-and-methods 参数可采用以下形式之一:

(declare declarations)

声明形式(declare form),具体说明参见 declare 形式

(:argument-precedence-order &rest args)

该形式会影响适用方法组合时的排序规则。默认情况下,组合过程中比较两个方法时,会从左到右检查方法参数,第一个参数特化符更具体的方法会排在前面;而该形式定义的顺序会覆盖这一默认规则 —— 参数检查顺序将按照此形式中 args 的排列顺序执行,而非从左到右。

(:method [qualifiers…] args &rest body)

该形式定义一个方法,功能与 cl-defmethod 一致。

Macro: cl-defmethod name [extra] [qualifier] arguments [&context (expr spec)…] &rest [docstring] body

该宏为名为 name 的泛型函数定义一个具体实现。实现代码由 body(函数体)提供;若存在 docstring,则其为该方法的文档字符串。 arguments(参数列表)需满足两个要求:一是实现同一泛型函数的所有方法,其参数列表必须完全一致;二是必须与该泛型函数的参数列表匹配。该参数列表以 (arg spec) 的形式提供参数特化符—— 其中 argcl-defgeneric 调用中指定的参数名,spec 可为以下特化符形式之一:

type

此特化符要求参数必须是指定的 type(类型),即下文所述类型层级中的某一种类型。

(eql object)

此特化符要求参数必须与指定的 object(对象)满足 eql 相等性。

(head object)

参数必须是一个 cons 单元,且其 car 部分与 object 满足 eql 相等性。

struct-type

参数必须是通过 cl-defstruct 定义的、名为 struct-type 的类的实例(see Structures in Common Lisp Extensions for GNU Emacs Lisp),或该类任一子类的实例。

方法定义中可以使用一个新的参数列表关键字 &context,它用于引入额外的环境特化符,在方法运行时对当前环境进行检测。该关键字应出现在必选参数列表之后,但在任何 &rest&optional 关键字之前。 &context 特化符的写法与普通参数特化符非常相似 — 形式为 (expr spec) — 区别在于:expr 是要在当前上下文中求值的表达式,而 spec 是用于比较的值。例如,&context (overwrite-mode (eql t)) 会让该方法仅在 overwrite-mode 开启时才适用。&context 关键字后可以跟随任意数量的环境特化符。由于环境特化符不属于泛型函数的参数签名,不需要它们的方法中可以省略。

类型特化符 (arg type) 可以指定下表中的一种 系统类型(system types)。当指定了一个父类型时,任何属于其更具体的子类型、孙类型、重孙类型等的参数,都将视为兼容。

integer

Parent type: number.

number
null

Parent type: symbol

symbol
string

Parent type: array.

array

Parent type: sequence.

cons

Parent type: list.

list

Parent type: sequence.

marker
overlay
float

Parent type: number.

window-configuration
process
window
subr
compiled-function
buffer
char-table

Parent type: array.

bool-vector

Parent type: array.

vector

Parent type: array.

frame
hash-table
font-spec
font-entity
font-object

可选的 extra 部分以 ‘:extra string’ 的形式书写,它允许你为相同的特化符与限定符添加更多方法,这些方法通过 string 加以区分。

可选的 qualifier(限定符)用于对多个适用方法进行组合。如果不指定该参数,则定义的方法为 主方法(primary method),负责为经过特化的参数提供泛型函数的主要实现。你也可以将以下值之一用作 qualifier,以定义 辅助方法(auxiliary methods)

:before

该辅助方法会在主方法之前执行。更准确地说,所有 :before 方法都会按最具体优先的顺序在主方法之前运行。

:after

该辅助方法会在主方法之后执行。更准确地说,所有此类方法都会按最具体最后的顺序在主方法之后运行。

:around

该辅助方法会 替代 主方法执行。此类方法中最具体的那个会先于其他所有方法执行。这类方法通常会使用下文介绍的 cl-call-next-method 来调用其他辅助方法或主方法。

使用 cl-defmethod 定义的函数无法通过添加 interactive 形式变为交互式函数(即命令,see Defining Commands)。若你需要一个多态命令,建议先定义一个普通命令,再让该命令调用通过 cl-defgenericcl-defmethod 定义的多态函数。

每次调用泛型函数时,它都会构建一个 有效方法(effective method)—— 该方法通过组合为当前函数定义的所有适用方法,来处理此次调用。查找适用方法并生成有效方法的过程被称为 分派(dispatch)。 适用方法指的是:其所有特化符均与调用时传入的实际参数兼容的方法。由于所有参数都必须与特化符兼容,因此所有参数都会共同决定某个方法是否适用。显式对多个参数进行特化的方法被称为 多分派方法(multiple-dispatch methods)

适用的方法会按照其组合执行的顺序进行排序。最左侧参数特化器(argument specializer) 最为具体的方法会排在该顺序的首位。(如上文所述,在 cl-defmethod 中指定 :argument-precedence-order 会覆盖这一默认排序规则。)若方法体调用了 cl-call-next-method,则会执行下一个次具体的方法。如果存在适用的 :around方法,其中最具体的 :around 方法会最先执行;该方法应调用 cl-call-next-method 以执行其余较不具体的 :around 方法。接下来, :before 方法会按其具体程度从高到低执行,随后执行主方法(primary method),最后 :after 方法会按其具体程度从低到高(即反向)执行。

Function: cl-call-next-method &rest args

当在主方法或 :around 辅助方法的词法作用域内调用此函数时,它会为同一个泛型函数调用下一个适用的方法。通常情况下,调用时不传入任何参数,这意味着会使用调用当前方法的同一组参数来调用下一个适用的方法。若传入了参数,则会改用指定的参数执行。

Function: cl-next-method-p

此函数在主方法或 :around 辅助方法的词法作用域内被调用时,若存在可调用的下一个方法,则返回非 nil 值;否则返回 nil


emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike