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

4 methods inheritance common-lisp reusability clos

比方说,有一种通用的方法incx.有两个版本incx.一个专门的类型a,一个专门的类型b.Type b是的子类a.您将获得一个类型的对象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))
Run Code Online (Sandbox Code Playgroud)

正如CLOS所承诺的,这称为最专业的方法:

* (inc r) 
* (describe r)
    ..
  Slots with :INSTANCE allocation:
    X  = 0
    Y  = 1
Run Code Online (Sandbox Code Playgroud)

但在这种特殊情况下,(不是一般)我想要的是访问不太专业的版本.说出类似的话:

(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
Run Code Online (Sandbox Code Playgroud)

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

在这个被删除的代码中,我确实需要类似的东西call-next-method,但是用于调用补充方法.我们需要调用其补充方法,而不是在下一个不太专业的类中调用同名方法,而是使用不同的名称.补充方法也是专门的,但调用这个专门的版本是行不通的 - 原因call-next-method可能与之相同.并非总是专用于超类的必需方法具有相同的名称.

(call-next-method my-complement)  ; doesn't work, thinks my-complement is an arg
Run Code Online (Sandbox Code Playgroud)

这是另一个例子.

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

(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
  )
Run Code Online (Sandbox Code Playgroud)

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

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

Dir*_*irk 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))
Run Code Online (Sandbox Code Playgroud)

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

(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? |#)))
Run Code Online (Sandbox Code Playgroud)

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


cor*_*ump 14

您的问题包含两个问题:

  1. 如何调用特定的有效方法?
  2. 如何在电子模拟的情况下避免复制粘贴?

这个答案是我的另一个答案的合并,部分受到了Dirk对具体例子的良好答案的启发.我将首先讨论问题(调用特定方法)并解释为什么你应该尝试另一种方法,特别是你的例子.

召集有效的方法

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

(ql:quickload :closer-mop)
Run Code Online (Sandbox Code Playgroud)

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

(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)))
Run Code Online (Sandbox Code Playgroud)

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

现在,我们计算类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))))
Run Code Online (Sandbox Code Playgroud)

在这里你有两种不同的行为:

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

=> (1 2)
Run Code Online (Sandbox Code Playgroud)

不建议这样做.CLOS提供了一种组合有效方法的方法,您应该尝试按预期使用它而不是劫持它.实际上,假设我评估以下内容:

(defmethod foo :before ((i a)) (print "Before A"))
Run Code Online (Sandbox Code Playgroud)

foo通用的功能,叫上一个实例çç,将打印字符串.但是当%foo-as-bc上使用时,没有打印字符串,即使我们正在调用函数as-if c而不是b的实例, 并且该方法专门用于a.

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

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

考虑Liskov替代原则.过度使用继承来共享代码(即实现细节)而不是多态,这就是"赞成组合而不是继承"这样的建议.请参阅 "赞成组合而不是继承"的概念来自何处?代码气味:继承滥用有关这方面的更多细节.

使用功能

在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)))))
Run Code Online (Sandbox Code Playgroud)

这个问题已经解决了,希望可以解读:在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)))
Run Code Online (Sandbox Code Playgroud)

注意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))
Run Code Online (Sandbox Code Playgroud)

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

(defmethod (setf spin) (new-value (e strange-electron))
  (funcall #'(setf spin) new-value (simple-electron e)))
Run Code Online (Sandbox Code Playgroud)

那很棒.现在,我们可以定义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))))))
Run Code Online (Sandbox Code Playgroud)

这与您原始问题中的方程式相同,但专门用于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)))
Run Code Online (Sandbox Code Playgroud)

你的目标之一是拥有一个CLOS接口,而不是一个"静态接口",这就是这里的情况.

结论

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

常见的代码可以通过定期的功能共享,就像是一直在做(为方便定义总是).或者,更喜欢组合物.