CLOS:如何调用不太具体的方法?

CLOS: how to call a less specific method?

有一个通用方法,比如incxincx有两个版本。一种专攻 a 类型,一种专攻 b 类型。类型 ba 的子 class。给定了一个 b 类型的对象,派生类型 - 但您想调用专门针对 a 类型的方法。如果还没有专用于类型 b 的同名方法,您可以轻松地做到这一点,但是,唉,有这样一个方法。

那么在这种情况下如何调用a类型特化的方法呢?

(defclass a () ((x :accessor x :initform 0)))
(defclass b (a) ((y :accessor y :initform 0)))

(defgeneric inc (i))

(defmethod inc ((i a)) (incf (x i)))
(defmethod inc ((i b)) (incf (y i)))

(defvar r (make-instance 'b))

正如 CLOS 所承诺的那样,这调用了最专业的方法:

* (inc r) 
* (describe r)
    ..
  Slots with :INSTANCE allocation:
    X  = 0
    Y  = 1

但在这种特殊情况下,(不是一般情况下)我想要的是访问不太专业的版本。像这样说:

(inc (r a)) ; crashes and burns of course, no function r or variable a
(inc a::r)  ; of course there is no such scoping operator in CL

我看到 call-next-method 函数可以在一个专门的方法中使用来获得下一个不太专门的方法,但这不是这里想要的。

在被删掉的代码中,我确实需要类似于 call-next-method 的东西,但用于调用补充方法。而不是在下一个不太专业的 class 中调用同名方法,我们需要调用其互补方法,该方法具有不同的名称。补充方法也是专门的,但是调用这个专门的版本是行不通的——原因与 call-next-method 可能包含在内的原因大致相同。专用于 super class 的所需方法并不总是具有相同的名称。

(call-next-method my-complement)  ; doesn't work, thinks my-complement is an arg

这是另一个例子

有一个描述电子属性的基础 class 和一个描述 "strange-electron" 属性的派生 class。专门针对奇怪电子的方法希望调用专门针对电子的方法。为什么?因为这些方法为程序完成了正常的电子部分工作。奇怪电子的非电子部分几乎是微不足道的,或者更确切地说,如果它不复制电子代码的话:

(defgeneric apply-velocity (particle velocity))
(defgeneric flip-spin (particle))

;;;; SIMPLE ELECTRONS

(defclass electron ()
  ((mass
      :initform 9.11e-31
      :accessor mass)
   (spin
      :initform -1
      :accessor spin)))

(defmacro sq (x) `(* ,x ,x))

(defmethod apply-velocity ((particle electron) v)
  ;; stands in for a long formula/program we don't want to type again:
  (setf (mass particle) 
        (* (mass particle) (sqrt (- 1 (sq (/ v 3e8)))))))

(defmethod flip-spin ((particle electron))
  (setf (spin particle) (- (spin particle))))

;;;; STRANGE ELECTRONS

(defclass strange-electron (electron)
  ((hidden-state
      :initform 1
      :accessor hidden-state)))

(defmethod flip-spin ((particle strange-electron))
  (cond
    ((= (hidden-state particle) 1)
     (call-next-method)

     ;; CALL ELECTRON'S APPLY-VELOCITY HERE to update
     ;; the electron. But how???
     )
    (t nil)))

;; changing the velocity of strange electrons has linear affect!
;; it also flips the spin without reguard to the hidden state!
(defmethod apply-velocity ((particle strange-electron) v)
  (setf (mass particle) (* (/ 8 10) (mass particle)))

  ;; CALL ELECTRON'S SPIN FLIP HERE - must be good performance,
  ;; as this occurs in critical loop code, i.e compiler needs to remove
  ;; fluff, not search inheritance lists at run time
  )

这一切都归结为一个简单的问题:

如果定义了更专业的方法,如何调用不太专业的方法?

使用 MOP(MetaObect 协议)可能是可行的。似乎 compute-applicable-methods 可能正是您想要的。

使用 change-class.

也可能玩出相当可怕的把戏

请注意,CLOS 中的方法不是 "methods on classes",而是 "methods on generic functions"。所以不能真正调用"a method of a different name, in the parent class",只能调用不同的泛型函数。

您的问题包含两个问题:

  1. 如何调用具体有效的方法?
  2. electron模拟时如何避免复制粘贴?

这个答案是我的另一个答案的合并,部分灵感来自 的具体示例。我将首先介绍所问的问题(调用特定方法)并解释为什么您应该尝试另一种方法,特别是对于您的示例。

调用有效的方法

是的,您可以调用与方法关联的函数而不是通用函数。 对于便携式方法,首先加载 closer-mop:

(ql:quickload :closer-mop)

定义一些 classes 和一个简单的泛型函数:

(defclass a () ())
(defclass b (a) ())
(defclass c (b) ())
(defgeneric foo (x)
  (:method ((x a)) 0)
  (:method ((x b)) (+ (call-next-method) 1))
  (:method ((x c)) (* (call-next-method) 2)))

我们有一个 class 层次结构 (a < b < c) 和一个仅在第一个参数上调度的通用函数。

现在,我们计算 class b 的适用方法,并使用结果列表定义一个函数,该函数调用 foo 专用的有效方法在 b.

(destructuring-bind (method . next)
    (closer-mop:compute-applicable-methods-using-classes
     #'foo
     (list (find-class 'b)))
  (let ((fn (closer-mop:method-function method)))
    (defun %foo-as-b (&rest args)
      (funcall fn args next))))

这里有两种不同的行为:

(let ((object (make-instance 'c)))
  (list
    (%foo-as-b object)
    (foo object))

=> (1 2)

但是不推荐。 CLOS 提供了一种组合有效方法的方法,您应该尝试按预期使用它而不是劫持它。 事实上,假设我评估以下内容:

(defmethod foo :before ((i a)) (print "Before A"))

c 的实例 c 上调用的 foo 泛型函数将打印字符串。但是当在 c 上使用 %foo-as-b 时,不会打印任何字符串,即使我们正在调用该函数,因为如果 c 是一个b 的实例,该方法专用于 a.

这当然是因为compute-applicable-methods-using-classes取决于调用时已知的方法集。在这种情况下,函数 %foo-as-b 仍在使用过时的方法列表。如果您定义多个这样的函数或专注于多个 classes,效果会被放大。如果你想始终保持 %foo-as-b 与你的环境同步,你需要在每次调用此函数时重新计算列表(而不是使用 let-over-lambda,你会重新计算 lambda 中的值) . 另一种可能性是在 CLOS 中引入钩子以在需要时重新计算函数,但这是疯狂的。

不要过度使用继承来共享代码

考虑 Liskov substitution principle。 过度使用继承来共享代码(即实现细节)而不是多态性是像 “Favor Composition over Inheritance” 这样的建议的来源。 看 Where does this concept of “favor composition over inheritance” come from?Code Smell: Inheritance Abuse 了解更多详情。

使用函数

在可以找到 base::method() 的 C++ 中,您只是调用了一个具有相似名称的不同函数:当您告诉编译器您要调用哪个方法时,没有动态调度,所以这实际上就像你调用了一个常规函数一样。

根据您的要求,我会写如下。它基于 Dirk 的版本,并使用辅助内联局部函数,当你想避免重复时,这些函数是完全足够的:

(defclass electron ()
  ((mass :initform 9.11e-31 :accessor mass)
   (spin :initform -1 :accessor spin)))

(defclass strange-electron (electron)
  ((hidden-state :initform 1 :accessor hidden-state)))

(let ((light-speed 3e8)
      (mysterious-velocity 0d0))
  (flet ((%flip (p)
           (setf (spin p) (- (spin p))))
         (%velocity (p v)
           (setf (mass p)
                 (* (mass p)
                    (sqrt
                     (- 1 (expt (/ v light-speed) 2)))))))
    (declare (inline %flip %velocity))
    
    (defgeneric flip-spin (particle)
      (:method ((p electron))
        (%flip p))
      (:method ((p strange-electron))
        (when (= (hidden-state p) 1)
          (call-next-method)
          (%velocity p mysterious-velocity))))

    (defgeneric apply-velocity (particle velocity)
      (:method ((p electron) v)
        (%velocity p v))
      (:method ((p strange-electron) v)
        (setf (mass p)
              (* (/ 8 10) (mass p)))
        (%flip p)))))

问题 已解决 并且希望它具有可读性:不需要在 CLOS 中破解其他东西。不同方法共享的辅助函数很容易识别,如果您需要重新编译其中任何一个,则必须重新编译整个表单,这确保了 classes 之间的现有耦合在所有方法中都被考虑在内.

使用组合

如果我们应用上述建议并改用组合会怎样? 让我们更改您的 strange-electron,使其 包含 一个 simple-electron。就实际电子而言,这听起来可能很奇怪,但如果我们考虑用于模拟的物体,这是有道理的;另外,请注意,在您的问题中,您实际上写了一个 “电子部分”“奇怪电子的非电子部分”。首先,主要 classes:

;; Common base class
(defclass electron () ())

;; Actual data for mass and spin
(defclass simple-electron (electron)
  ((mass :initform 9.11e-31 :accessor mass)
   (spin :initform -1 :accessor spin)))

;; A strange electron with a hidden state
(defclass strange-electron (electron)
  ((simple-electron :accessor simple-electron :initarg :electron)
   (hidden-state :initform 1 :accessor hidden-state)))

请注意 strange-electron 不再继承自 simple-electron(我们不需要存储单独的质量和自旋),而是包含 simple-electron 的一个实例。 另请注意,我们添加了一个公共 electron 基础 class,在这种情况下这并不是绝对必要的。 我将跳过定义通用函数的部分,只描述方法。 为了get/set那些奇怪电子的质量和自旋,我们不得不委托给内部对象:

(macrolet ((delegate (fn &rest args)
             `(defmethod ,fn (,@args (e strange-electron))
                (funcall #',fn ,@args (simple-electron e)))))
  (delegate mass)
  (delegate spin)
  (delegate (setf mass) new-value)
  (delegate (setf spin) new-value))

在我们继续之前,上面的代码做了什么?如果我们展开macrolet中的最后一个形式,即带有(setf spin)的形式,我们得到一个设置内部粒子槽的方法:

(defmethod (setf spin) (new-value (e strange-electron))
  (funcall #'(setf spin) new-value (simple-electron e)))

太好了。现在,我们可以非常简单地定义 flip-spinapply-velocity。 基本行为与 simple-electron class:

(defmethod flip-spin ((e simple-electron))
  (setf (spin e) (- (spin e))))

(defmethod apply-velocity ((e simple-electron) velocity)
  (setf (mass e)
        (* (mass e)
           (sqrt
            (- 1 (expt (/ velocity +light-speed+) 2))))))

这与您原来的问题中的等式相同,但专门针对 simple-electron。对于奇怪的电子,你依赖内部对象:

(defmethod flip-spin ((e strange-electron))
  (when (= (hidden-state e) 1)
    (flip-spin (simple-electron e))
    (apply-velocity (simple-electron e) 0d0)))

(defmethod apply-velocity ((e strange-electron) velocity)
  (setf (mass e) (* (/ 8 10) (mass e)))
  (flip-spin (simple-electron e)))

您的 objective 之一是拥有 CLOS 接口而不是“静态接口”,这里正是这种情况。

结论

显式调用不太具体的方法是一种代码味道。我不排除在某些情况下它可能是一种明智方法的可能性,但我建议首先考虑替代设计。

可以通过常规函数共享公共代码,就像以前一样(为了方便定义 always)。 或者,prefer composition.

我更喜欢这里的显式方法:

(defun actually-inc-a (value) (incf (x value)))
(defun actually-inc-b (value) (incf (y value)))

(defmethod inc ((object a)) (actually-inc-a object))
(defmethod inc ((object b)) (actually-inc-b object))

即,将要共享的实现部分放入单独的函数中。

(defun apply-velocity-for-simple-electron (particle v)
  (setf (mass particle) (* (mass particle) (sqrt (- 1 (sq (/ v 3e8)))))))

(defun flip-spin-for-simple-electron (particle)
  (setf (spin particle) (- (spin particle))))

(defmethod apply-velocity ((particle electron) v)
  (apply-velocity-for-simple-electron particle v))

(defmethod flip-spin ((particle electron))
  (flip-spin-for-simple-electron particle))

(defmethod apply-velocity ((particle strange-electron) v)
  (setf (mass particle) (* (/ 8 10) (mass particle)))
  (flip-spin-for-simple-electron particle))

(defmethod flip-spin ((particle strange-electron))
  (when (= (hidden-state particle) 1)
    (call-next-method)
    (apply-velocity-for-simple-electron particle #| Hu? What's the V here? |#)))

考虑到我对电子一无所知,无论是普通电子还是奇怪电子,是否自旋,我真的想不出这些基本辅助函数的有意义的名称。但除此之外...

PS:我知道这个答案来晚了,但我仍然觉得它是一个强有力的选择,尚未在其他答案中得到考虑。


注意:对于专门针对单个参数的方法,可以说下一个方法是专门针对为专门参数提供的参数的超类的方法.

但是,这并不一般,例如,一个方法专门处理一个参数,另一个方法专门处理另一个参数,或者方法专门处理多个参数参数.


尽管如此,对于你手头的实际问题,你可以使用另一种方法,即使用一个特殊的变量来告诉你自己的方法简单地call-next-method:

(defvar *strange-electron-bypass* nil)

(defmethod flip-spin ((particle strange-electron))
  (let ((bypass *strange-electron-bypass*)
        (*strange-electron-bypass* nil))
    (cond (bypass
           (call-next-method))
          ((= (hidden-state particle) 1)
           (call-next-method)
           (let ((*strange-electron-bypass* t))
             ;; where does v come from?
             (apply-velocity particle v)))
          (t
           nil))))

(defmethod apply-velocity ((particle strange-electron) v)
  (let ((bypass *strange-electron-bypass*)
        (*strange-electron-bypass* nil))
    (cond (bypass
           (call-next-method))
          (t
           (setf (mass particle)
                 (* (/ 8 10) (mass particle)))
           (let ((*strange-electron-bypass* t))
             (flip-spin particle))))))

如果您只专注于 类,那么在 apply-velocity (strange-elector t) 中调用 flip-spin (strange-electron) 的性能不会受到太大影响。在大多数(如果不是全部)CLOS 实现中,在这种情况下,适用的方法将根据参数的 类 被记忆(缓存),因此只有第一次调用 strange-electron 本身的实例才会支付计算适用方法的价格。

这种方法的优点是它是可推广的,因为它会调用下一个最具体的方法,而且它不需要搞乱 CLOS,这通常意味着失去由 Common Lisp 实现执行的优化。

编辑:如您所见,变量 *strange-electron-bypass* 在方法入口处重新绑定到 nil 以支持递归、相互或其他方式。在这种情况下,没有递归,但如果您想将此解决方案推广到可能存在递归的其他情况(即同一方法在调用堆栈中适用两次),尤其是在组合情况下,这些方法将是可重入的。

Dirk 的回答有几个问题可以解决,如下所示。

第一,不泛化不成为新的静态对象系统。尝试泛化时,人们很快就会遇到这样一个事实,即属于同一泛型定义的所有方法都具有相同的名称。为了解决这个问题,剩下的就是给函数改名以反映它们的类型签名(根据 Stroustrup 著名的宏处理器)。

其次,泛化后它变成了一个单独的静态面向对象系统。作为静态系统,它不能很好地与 CLOS 配合使用。它成为混合范式的案例。

但是,Dirk 避免代码重复的方法可以保留在本地,而无需将辅助例程导出到界面。这可以通过将它们包装在 CLOS 方法中来实现。然后,这些 CLOS 方法成为专业化树中的分支,其中一个可以独立于其他分支进行专业化。名称更改代表一个分支而不是类型签名(更易于管理)。

所以这是应用于inc示例的封装辅助功能方法。请注意,inc-a 变成了一个不太专门的函数,可以被其他人调用,包括专门用于继承的 b class 的方法,因为 b class 中没有方法进一步专门化它(与 inc 不同)。

(defclass a () ((x :accessor x :initform 0)))
(defclass b (a) ((y :accessor y :initform 0)))

(defgeneric inc (i))
(defgeneric inc-a (i)) ; same as inc, but won't be further specialized

(defmacro inc-a-stuff (i) ; this is not exported! not an interface
  `(incf (x ,i))
  )

(defmethod inc ((i a)) (inc-a-stuff i))
(defmethod inc ((i b)) (incf (y i)))

;; provides a method to generalize back to class a
;; this method does not get further specialization by b, thus
;; remains a window into the "a part"
(defmethod inc-a ((i a)) (inc-a-stuff i))

(defvar r (make-instance 'b))

(inc r) ; all good, increments y

;;(inc (r a)) ; ah how do you get this?
;;
(inc-a r) ; 

(describe r)

#|
Slots with :INSTANCE allocation:
  X  = 1
  Y  = 1
|#

此解决方案对于对象架​​构的动态更改没有危险。 IE。它在 CLOS 中工作。