【发布时间】:2012-03-22 03:04:48
【问题描述】:
我正在研究编程语言设计,我对如何用多方法泛型函数范式替换流行的单调度消息传递 OO 范式感兴趣。在大多数情况下,这似乎很简单,但我最近陷入困境,希望能得到一些帮助。
在我看来,消息传递 OO 是一种解决两个不同问题的解决方案。我在下面的伪代码中详细解释了我的意思。
(1) 解决调度问题:
=== 在文件 animal.code ===
- Animals can "bark"
- Dogs "bark" by printing "woof" to the screen.
- Cats "bark" by printing "meow" to the screen.
=== 在文件 myprogram.code ===
import animal.code
for each animal a in list-of-animals :
a.bark()
在这个问题中,“bark”是一种具有多个“分支”的方法,这些“分支”的操作取决于参数类型。我们为每个我们感兴趣的参数类型(狗和猫)实现一次“吠叫”。在运行时,我们能够遍历动物列表并动态选择要采用的适当分支。
(2) 解决命名空间问题:
=== 在文件 animal.code ===
- Animals can "bark"
=== 在文件 tree.code ===
- Trees have "bark"
=== 在文件 myprogram.code ===
import animal.code
import tree.code
a = new-dog()
a.bark() //Make the dog bark
…
t = new-tree()
b = t.bark() //Retrieve the bark from the tree
在这个问题中,“bark”实际上是两个概念上不同的函数,它们只是碰巧具有相同的名称。参数的类型(无论是狗还是树)决定了我们实际指的是哪个函数。
多方法优雅地解决了问题 1。但我不明白它们如何解决问题 2。例如,上面两个示例中的第一个可以直接转换为多方法:
(1) Dogs and Cats 使用多种方法
=== 在文件 animal.code ===
- define generic function bark(Animal a)
- define method bark(Dog d) : print("woof")
- define method bark(Cat c) : print("meow")
=== 在文件 myprogram.code ===
import animal.code
for each animal a in list-of-animals :
bark(a)
关键点是方法 bark(Dog) 在概念上与 bark(Cat) 相关。第二个例子没有这个属性,这就是为什么我不明白multimethods是如何解决命名空间问题的。
(2) 为什么多方法不适用于动物和树木
=== 在文件 animal.code ===
- define generic function bark(Animal a)
=== 在文件 tree.code ===
- define generic function bark(Tree t)
=== 在文件 myprogram.code ===
import animal.code
import tree.code
a = new-dog()
bark(a) /// Which bark function are we calling?
t = new-tree
bark(t) /// Which bark function are we calling?
在这种情况下,应该在哪里定义泛型函数?它应该在动物和树之上的顶层定义吗?将 bark for animal 和 tree 视为相同泛型函数的两种方法是没有意义的,因为这两个函数在概念上是不同的。
据我所知,我还没有发现任何过去的工作可以解决这个问题。我看过 Clojure 多方法和 CLOS 多方法,它们有同样的问题。我正在祈祷,希望能找到一个优雅的解决方案,或者一个有说服力的论据,说明为什么它在实际编程中实际上不是问题。
如果问题需要澄清,请告诉我。我认为这是一个相当微妙(但很重要)的观点。
感谢您的理智、Rainer、Marcin 和 Matthias 的回复。我理解您的回复并完全同意动态调度和命名空间解析是两件不同的事情。 CLOS 不会将这两个想法混为一谈,而传统的消息传递 OO 则可以。这也允许将多方法直接扩展到多继承。
我的问题具体是在合意的情况下。
下面是我的意思的一个例子。
=== 文件:XYZ.code ===
define class XYZ :
define get-x ()
define get-y ()
define get-z ()
=== 文件:POINT.code ===
define class POINT :
define get-x ()
define get-y ()
=== 文件:GENE.code ===
define class GENE :
define get-x ()
define get-xx ()
define get-y ()
define get-xy ()
==== 文件:my_program.code ===
import XYZ.code
import POINT.code
import GENE.code
obj = new-xyz()
obj.get-x()
pt = new-point()
pt.get-x()
gene = new-point()
gene.get-x()
由于命名空间解析与分派的混合,程序员可以天真地对所有三个对象调用 get-x()。这也是完全明确的。每个对象“拥有”自己的一组方法,因此程序员的意思不会混淆。
将此与多方法版本进行对比:
=== 文件:XYZ.code ===
define generic function get-x (XYZ)
define generic function get-y (XYZ)
define generic function get-z (XYZ)
=== 文件:POINT.code ===
define generic function get-x (POINT)
define generic function get-y (POINT)
=== 文件:GENE.code ===
define generic function get-x (GENE)
define generic function get-xx (GENE)
define generic function get-y (GENE)
define generic function get-xy (GENE)
==== 文件:my_program.code ===
import XYZ.code
import POINT.code
import GENE.code
obj = new-xyz()
XYZ:get-x(obj)
pt = new-point()
POINT:get-x(pt)
gene = new-point()
GENE:get-x(gene)
因为 XYZ 的 get-x() 与 GENE 的 get-x() 没有概念上的关系,所以它们被实现为单独的通用函数。因此,最终程序员(在 my_program.code 中)必须明确限定 get-x() 并告诉系统 他实际上要调用哪个 get-x()。
确实,这种显式方法更清晰,更容易推广到多重分派和多重继承。但是使用(滥用)调度来解决命名空间问题是消息传递 OO 的一个非常方便的特性。
我个人觉得我自己的 98% 的代码都使用单调度和单继承来充分表达。我使用 dispatch 进行命名空间解析的便利性比使用多分派要多得多,所以我不愿意放弃它。
有没有办法让我两全其美?如何避免在多方法设置中明确限定我的函数调用?
似乎共识是这样的
- 多方法解决了调度问题,但不攻击命名空间问题。
- 概念上不同的函数应该有不同的名称,用户应该手动限定它们。
然后我相信,在单继承单分派足够的情况下,消息传递 OO 比泛型函数更方便。
这听起来像是开放研究。如果一种语言要为多方法提供一种也可用于命名空间解析的机制,那会是一个理想的特性吗?
我喜欢泛型函数的概念,但目前觉得它们经过优化,可以让“非常难的事情变得不那么难”,而牺牲了“让琐碎的事情有点烦人”。由于大部分代码都是微不足道的,我仍然认为这是一个值得解决的问题。
【问题讨论】:
-
您在这里看到了什么问题?该方法应该适用于这两种类型。
-
事实上,恰恰相反:多方法获得正确的命名空间。特别是,它们使多重继承工作。想想当你发现你需要定义一个树人和动物模型的树人类时会发生什么。使用多方法没有问题,因为它们的命名空间独立于类。使用类命名空间方法会产生冲突。
标签: clojure lisp common-lisp language-design multimethod