CLOS:如何调用不太具体的方法?
CLOS: how to call a less specific method?
有一个通用方法,比如incx
。 incx
有两个版本。一种专攻 a
类型,一种专攻 b
类型。类型 b
是 a
的子 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",只能调用不同的泛型函数。
您的问题包含两个问题:
- 如何调用具体有效的方法?
- 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-spin
和 apply-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 中工作。
有一个通用方法,比如incx
。 incx
有两个版本。一种专攻 a
类型,一种专攻 b
类型。类型 b
是 a
的子 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",只能调用不同的泛型函数。
您的问题包含两个问题:
- 如何调用具体有效的方法?
- 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-spin
和 apply-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 中工作。