理解Common Lisp中的通用函数?

3
在这个答案中,用户给出了一个非常清晰的例子,说明了类和方法如何协同工作。
我将在此重印这个例子:

(defclass human () ())
(defclass dog () ())

(defmethod greet ((thing human))
  (print "Hi human!"))

(defmethod greet ((thing dog))
  (print "Wolf-wolf dog!"))

(defparameter Anna (make-instance 'human))
(defparameter Rex (make-instance 'dog))

(greet Anna) ;; => "Hi human"
(greet Rex)  ;; => "Wolf-wolf dog!"


我的问题是,以相同的示例为例:
  1. 创建通用函数会增加什么价值?
  2. 通用函数有什么用处?它们是否像其他面向对象语言中的实例提供结构?
似乎通用函数是隐式地在后台创建的(不确定百分之百)。我注意到当我尝试创建一个具有不同参数结构的方法时,会出现“通用函数错误”。

3
https://cl-cookbook.sourceforge.net/clos-tutorial/ - Rainer Joswig
3
https://www.algo.be/cl/documents/clos-guide.html - Rainer Joswig
3个回答

7

创建通用函数的价值是什么?

我喜欢明确声明通用函数,因为可以添加与通用函数相关的文档和声明(优化速度/空间/调试),以及其他细节,如方法组合(即当您有多个适用于给定调用的方法时,定义哪些方法以及以何种顺序执行)。例如,在这里我可以将talk定义为具有progn方法组合(所有方法都像封装在progn表单中一样执行):

(defgeneric talk (subject)
  (:documentation "Say something to standard output")
  (:method-combination progn))

这是一个有点牵强的例子,但让我们开始吧:

(defclass human () ())
(defmethod talk progn ((a human))
  (print "hello"))

(defclass wolf () ())
(defmethod talk progn ((a wolf))
  (print "owooooo!"))

(defclass werewolf (human wolf) ())

定义一个同时继承两个类的类意味着对该类实例的talk调用可以执行两个方法(按一定的拓扑顺序排序,称为方法解析顺序)。因此,使用此方法组合将执行所有方法:

* (talk (make-instance 'werewolf))
"hello" 
"owooooo!"

但是,我认为能够记录通用函数本身就足以使用defgeneric声明。

通用函数有什么用处?

如果您将talk定义为通用函数,则允许任何类参与调用talk的代码(例如库),这是一种在不关闭可能值集合的情况下允许扩展的方法,不像在函数中使用typecase,您只能列出预定义的一组案例。

例如,标准函数print-object在您的Lisp实现中的各个时间被调用(在检查器、REPL、调试器中),如果您想要,可以为自定义类型实现一个方法,而无需破解环境的内部。

它们是否像其他OO语言中提供结构的实例?

通用函数与其他OO语言不同,它们不与单个类或实例绑定。它们可以针对多个参数进行专业化1,这意味着没有一个“拥有”通用函数。


1. specialized的定义为如下:

specialize v.t.(一种通用函数)为了为通用函数定义一个方法,或者换句话说,通过为特定类或参数集合赋予其特定含义,以改善通用函数的行为。

背后的理念是方法可以更或少具体:一种具有两个参数ab的方法,它专门针对(a number)(b string)是比只专门针对(b vector)的方法更具体,而且方法在组合时实际上是从最具体到最不具体的排序方式(相应地从最不具体到最具体)。您甚至可以将一个函数专门针对(a (eql 10)),以仅覆盖参数a等于eql 10的特定情况。


我一直看到这个词“specialized”。但是在CLOS的特定上下文中,它是什么意思呢? - Vinn

4

有几个原因需要显式地定义泛型函数,而非让 defmethod 隐式定义。

其中一个总是适用的原因是您想编写良好的程序,良好的程序需要像函数的文档这样的东西来形成它们的重要部分。使用 CLOS 实现这一点的方法是使用 defgeneric:在 defgeneric 中您可以放置一个大型文档字符串或注释,其中告诉人们泛型函数的功能,为什么可能需要编写方法,可能存在哪些关于那些方法的限制(“不要编写特化于由 CL 定义的类”的方法)等等。

通过使用显式泛型函数,您可以以另一种方式改进程序,即使用泛型函数定义明确地说明方法必须拥有哪些参数:

(defgeneric expunge-bogon (bogon &key with-force))

声明所有 expunge-bogon 方法必须支持一个 with-force 关键字参数(它们也可以支持其他关键字参数,但必须支持这个)。

此外,有很多事情您只能通过指定通用函数来完成,例如指定非默认的方法组合。举个简单的例子,如果您使用 Tim Bradshaw 的 wrapping-standard 方法组合来确保您可以将一个方法包装在任何其他方法周围,那么您可以(使用他的另一个实用工具 destructuring-match,以一种可能不是其原本意图的方式)确保只允许在通用函数中指定的关键字参数(仅在运行时):

(defgeneric expunge-bogon (bogon &key with-force)
  (:method-combination wrapping-standard)
  (:method :wrapping (bogon &rest args)
   (destructuring-match args
     ((&key ((:with-force _)))
      (call-next-method))
     (otherwise
      (error "extra keyword arguments in ~S" args)))))

现在假设

(defclass entitled-billionaire ()
  ())

(defmethod expunge-bogon ((bogon entitled-billionaire)
                         &key (with-force t) (also-burn t))
  ...)

那么
> (expunge-bogon (make-instance 'entitled-billionaire) :also-burn nil)

Error: extra keyword arguments in (:also-burn nil)

3

DEFMETHOD会创建一个通用函数,如果不存在的话。

显式定义通用函数可以让您指定一些附加选项。因此,只有在想要首先指定这些选项时才需要它。它也可以作为文档,说明代码定义了哪些通用函数。

还可以指定通用函数并在一个`DEFGENERIC`表单中使用它来指定方法。不过,这种情况并不常见。

方法的参数列表必须遵循规则。例如,特定通用函数的方法的参数列表都需要具有相同数量的必需参数。

通用函数及其方法是CLOS中面向对象行为的基本构造。CLOS使用函数而不是虚拟方法或消息,这更适合Lisp。因此,CLOS通用函数也可以被传递并从其他函数返回。

通用函数也有点不寻常,因为通用函数和方法本身就是CLOS对象。通用函数既是一个FUNCTION,也是一个CLOS对象。


1
这也意味着我不需要明确地定义一个通用函数。我只需要根据我的实现思路创建方法和类即可? - Vinn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接