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

时间:2016-02-03 08:14:35

标签: inheritance methods common-lisp reusability clos

有一种通用的方法,比如incxincx有两个版本。一个专门针对类型a,一个专门针对类型b。类型ba的子类。您将获得类型为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的东西,但是用于调用补充方法。我们需要调用其补充方法,而不是在下一个不太专业的类中调用同名方法,而是使用不同的名称。补充方法也是专门的,但调用此专用版本不起作用 - 原因与call-next-method可能包含的原因相同。并不总是专用于超类的必需方法具有相同的名称。

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

这是另一个例子

有一个描述电子属性的基类和一个描述“奇怪电子”属性的派生类。专门研究特殊电子方法的奇怪电子的方法。为什么?因为这些方法为程序做正常的电子部分工作。奇怪电子的非电子部分几乎是微不足道的,或者更确切地说,如果它没有复制电子代码就会出现:

(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
  )

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

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

5 个答案:

答案 0 :(得分:16)

我更喜欢这里的明确方法:

(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? |#)))

鉴于,我对电子一无所知,无论是普通的还是奇怪的,旋转与否,我都无法想到那些基本辅助函数的有意义的名称。但除此之外......

答案 1 :(得分:14)

您的问题包含两个问题:

  1. 如何调用特定的有效方法?
  2. 如何在电子模拟的情况下避免复制粘贴?
  3. 这个答案是我的另一个答案的合并,部分受到Dirk's good answer的启发,具体的例子。我将首先讨论问题(调用特定方法)并解释为什么你应该尝试另一种方法,特别是你的例子。

    调用有效方法

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

    (ql:quickload :closer-mop)
    

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

    (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)))
    

    我们有一个类层次结构(一个< b< c< c< c< c< c< c< b< b< b< b< b< b< b< b> c)和一个泛型函数仅在第一个参数上调度。

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

    (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"))
    

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

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

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

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

    使用功能

    在C ++中,可以找到base::method(),你只是调用一个具有相似名称的不同函数:当你告诉你的编译器要调用哪个方法时,没有动态调度,所以这个事实上,如果你打电话给常规功能。

    根据您的要求,我会写下以下内容。它基于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中破解别的东西。可以很容易地识别由不同方法共享的辅助函数,如果需要重新编译它们,则必须重新编译整个表单,这样可以确保在所有方法中考虑类之间的现有耦合。

    使用构图

    如果我们应用上述建议并使用构图,会发生什么? 让我们更改您的strange-electron,使其包含一个simple-electron。对于实际电子而言,这可能听起来很奇怪,但如果我们考虑用于模拟的对象,这是有意义的;此外,请注意,在您的问题中,您实际上写过关于"电子部分" "奇怪电子的非电子部分" 。一,主要类:

    ;; 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基类,在这种情况下并不是绝对必要的。 我将跳过我们定义泛型函数的部分,并仅描述方法。 为了获得/设置那些奇怪电子的质量和旋转,我们必须委托内部对象:

    (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类相关联:

    (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)))
    

    你的目标之一是拥有一个CLOS界面,而不是一个静态界面",这就是这里的情况。

    结论

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

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

答案 2 :(得分:3)

可以使用MOP(MetaObect协议)。好像compute-applicable-methods似乎正是你想要的。

也许有可能使用change-class发挥相当可怕的技巧。

请注意,CLOS中的方法不是"类"上的方法,它们是"通用函数的方法"。因此,您无法真正调用"一个不同名称的方法,在父类"中,您只能调用不同的通用函数。

答案 3 :(得分:3)

PS :我知道这个答案已经很晚了,但我仍然认为这是一个尚未在其他答案中考虑的强大选择。

注意:对于专门针对单个参数的方法,可能有必要说下一个方法是专门针对专用参数提供的参数的超类的方法。

但是,这并不能保持一般,例如,一个方法专门针对一个参数,另一个方法专门针对另一个参数,或者使用专门针对多个参数的方法。 / p>

尽管如此,对于您手边的实际问题,您可以使用另一种方法,即使用特殊变量来简单地告诉您自己的方法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))))))

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

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

编辑:正如您所看到的,变量*strange-electron-bypass*在方法输入时反弹到nil以支持递归,相互或其他方式。在这种情况下,没有递归,但是如果你想将这个解决方案推广到可能存在递归的其他情况(即同一方法在调用堆栈中适用两次),特别是在组合案例中,方法将是可重入的。

答案 4 :(得分:1)

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

首先,它不会在不成为新的静态对象系统的情况下进行概括。在尝试泛化时,很快就会遇到属于同一通用定义的所有方法具有相同名称的事实。为了解决这个问题,我们可以给出反映其类型签名的函数munged(根据Stroustrup着名的宏处理器)。

其次,当概括时,它变成一个单独的静态面向对象系统。作为一个静态系统,它与CLOS不能很好地兼容。它成为混合范式的一种情况。

但是,Dirks避免代码重复的方法可以保持在本地,而无需将辅助例程导出到接口。这可以通过将它们包装在CLOS方法中来实现。然后,这些CLOS方法成为特化树中的分支,可以与其他分支分开专用。然后,名称更改表示分支而不是类型签名(更易于管理)。

所以这里是应用于inc示例的封装辅助函数方法。请注意,inc-a成为一个不太专业的函数,可以由其他人调用,包括专门用于继承的b类的方法,因为b类中的方法没有进一步专门化(与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
|#

此解决方案对于对象架​​构的动态更改没有任何危险。即它适用于CLOS。