multimethods如何解决命名空间问题?

时间:2012-03-04 07:27:52

标签: clojure lisp common-lisp language-design multimethod

我正在研究编程语言设计,我对如何用多方法泛型函数范例替换流行的单调度消息传递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"是一种具有多个"分支的方法"根据参数类型的不同而不同。我们实施" 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"实际上是两个概念上不同的函数,只是发生具有相同的名称。参数的类型(无论是狗还是树)决定了我们实际意味着什么功能。


Multimethods优雅地解决问题编号1.但我不明白他们如何解决问题编号2.例如,上述两个例子中的第一个可以直接翻译成多方法:

(1)使用多种方法的狗和猫

===在文件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)

关键是方法树皮(Dog)在概念上与树皮(Cat)相关。第二个示例没有此属性,这就是为什么我不理解多方法如何解决命名空间问题。

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

在这种情况下,应该在哪里定义泛型函数?它应该在顶层,动物和树木之上定义吗?将动物和树木的树皮视为同一通用函数的两种方法是没有意义的,因为这两种函数在概念上是不同的。

据我所知,我还没有发现任何过去的工作已经解决了这个问题。我看过Clojure多方法和CLOS多方法,他们也遇到了同样的问题。我正在交叉手指,希望能够找到问题的优雅解决方案,或者说明为什么它在实际编程中实际上不是问题。

如果问题需要澄清,请告诉我。我认为这是一个相当微妙(但很重要)的观点。


感谢回复理智,Rainer,Marcin和Matthias。我理解您的回复并完全同意动态调度和命名空间解析是两回事。 CLOS并没有把这两个想法混为一谈,而传统的消息传递OO就是这样。这也允许将多方法直接扩展到多重继承。

我的问题具体是在合并是期望的情况下。

以下是我的意思的一个例子。

=== file:XYZ.code ===

define class XYZ :
   define get-x ()
   define get-y ()
   define get-z ()

=== file:POINT.code ===

define class POINT :
   define get-x ()
   define get-y ()

=== file:GENE.code ===

define class GENE :
   define get-x ()
   define get-xx ()
   define get-y ()
   define get-xy ()

==== file: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()。这也是完全明确的。每个对象拥有"它有自己的一套方法,因此对于程序员的意思没有混淆。

将此与多方法版本对比:


=== file:XYZ.code ===

define generic function get-x (XYZ)
define generic function get-y (XYZ)
define generic function get-z (XYZ)

=== file:POINT.code ===

define generic function get-x (POINT)
define generic function get-y (POINT)

=== file: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)

==== file: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进行命名空间解析比使用多次调度更方便,所以我不愿意放弃它。

有没有办法让我两全其美?如何避免在多方法设置中明确限定函数调用的需要?


似乎共识就是那个

  • multimethods解决调度问题但不攻击命名空间问题。
  • 概念上不同的功能应该具有不同的名称,而 用户应该手动限定它们。

然后我相信,在单继承单调度足够的情况下,消息传递OO比通用函数更方便。

这听起来像是开放研究。如果一种语言为多种方法提供了一种机制,也可以用于命名空间解析,那么这是一个理想的特性吗?

我喜欢泛型功能的概念,但目前认为它们已经过优化,非常难以制造非常困难的东西。以牺牲微不足道的事情为代价而烦恼"。由于大多数代码都是微不足道的,我仍然认为这是一个值得解决的问题。

7 个答案:

答案 0 :(得分:19)

动态调度和命名空间解析是两回事。在许多对象系统中,类也用于名称空间。另请注意,类和命名空间通常都绑定到文件。因此,这些对象系统至少会混淆三件事:

  • 类定义及其插槽和方法
  • 标识符的命名空间
  • 源代码的存储单元

Common Lisp及其对象系统(CLOS)的工作方式不同:

  • 类不构成命名空间
  • 通用函数和方法不属于类,因此未在类
  • 中定义
  • 泛型函数被定义为顶级函数,因此不是嵌套的或本地的
  • 通用函数的标识符是符号
  • 符号有自己的名为packages
  • 的命名空间机制
  • 通用功能是开放的'。可以随时添加或删除方法
  • 泛型函数是第一类对象
  • mathods是一流的对象
  • 类和泛型函数也不与文件混淆。您可以在一个文件中或在任意数量的文件中定义多个类和多个通用函数。您还可以通过运行代码(因此不依赖于文件)或类似REPL(读取eval打印循环)来定义类和方法。

CLOS中的风格:

  • 如果功能需要动态调度且功能密切相关,则使用一个具有不同方法的通用函数
  • 如果有许多不同的功能,但有一个共同的名称,请不要将它们放在同一个通用功能中。创建不同的通用函数。
  • 具有相同名称的通用函数,但名称在不同包中的是不同的通用函数。

示例:

(defpackage "ANIMAL" (:use "CL")) 
(in-package "ANIMAL")

(defclass animal () ())
(deflcass dog (animal) ())
(deflcass cat (animal) ()))

(defmethod bark ((an-animal dog)) (print 'woof))
(defmethod bark ((an-animal cat)) (print 'meow)) 

(bark (make-instance 'dog))
(bark (make-instance 'dog))

请注意,类ANIMAL和包ANIMAL具有相同的名称。但这不是必要的。这些名称没有任何关联。 DEFMETHOD隐式创建了相应的泛型函数。

如果添加另一个包(例如GAME-ANIMALS),则BARK泛型函数将有所不同。除非这些包是相关的(例如一个包使用另一个包)。

从另一个包(Common Lisp中的符号命名空间),可以调用它们:

(animal:bark some-animal)

(game-animal:bark some-game-animal)

符号的语法为

PACKAGE-NAME::SYMBOL-NAME

如果包与当前包相同,则可以省略。

  • ANIMAL::BARK引用包BARK中名为ANIMAL的符号。请注意,有两个冒号。
  • AINMAL:BARK引用包BARK中的导出符号ANIMAL。请注意,只有一个冒号。 导出导入使用是为包及其符号定义的机制。因此,它们独立于类和泛型函数,但它可用于构造命名它们的符号的命名空间。

更有趣的情况是多方法实际用于泛型函数:

(defmethod bite ((some-animal cat) (some-human human))
  ...)

(defmethod bite ((some-animal dog) (some-food bone))
  ...)

以上使用了类CATHUMANDOGBONE。泛型函数应属于哪个类?特殊命名空间会是什么样的?

由于泛型函数会调度所有参数,因此将泛型函数与特殊命名空间混淆并使其成为单个类中的定义没有直接意义。

<强>动机:

开发人员在 Xerox PARC (对于 Common LOOPS )和 Symbolics 添加了80年代的通用函数给Lisp新口味。一个想要摆脱额外的调用机制(消息传递)并将调度带到普通(顶级)函数。 New Flavors有单个调度,但具有多个参数的泛型函数。对Common LOOPS的研究带来了多次调度。然后用标准化的CLOS取代新口味和普通LOOPS。然后将这些想法带到其他语言,如 Dylan

由于问题中的示例代码不使用通用函数提供的任何内容,因此看起来必须放弃一些东西。

当单个调度,消息传递和单个继承就足够了,那么泛型函数可能看起来像退一步。如上所述,其原因是人们不希望将所有类似的命名功能放入一个通用函数中。

(defmethod bark ((some-animal dog)) ...)
(defmethod bark ((some-tree oak)) ...)
看起来很相似,它们是两个概念上不同的行为。

但更多:

(defmethod bark ((some-animal dog) tone loudness duration)
   ...)

(defmethod bark ((some-tree oak)) ...)

现在突然,同一命名泛型函数的参数列表看起来不同。是否应该允许一个通用功能?如果没有,我们如何使用正确的参数在事物列表中的各种对象上调用BARK

在真正的Lisp代码中,泛型函数通常看起来要复杂得多,有几个必需和可选的参数。

在Common Lisp中,泛型函数不仅具有单一方法类型。有不同类型的方法和各种方法来组合它们。当它们真正属于某种通用函数时,将它们组合起来是有意义的。

由于泛型函数也是第一类对象,因此它们可以传递,从函数返回并存储在数据结构中。此时,泛型函数对象本身很重要,而不再是它的名字。

对于我有一个具有x和y坐标且可以作为一个点的对象的简单情况,我将从POINT类继承对象的类(可能是一些mixin) )。然后,我会在必要时将GET-XGET-Y符号导入某个命名空间。

还有其他语言与Lisp / CLOS更不同,它们尝试(编辑)支持多方法:

似乎有很多尝试将它添加到Java。

答案 1 :(得分:9)

“为什么多方法不起作用”的示例假定您可以在同一语言名称空间中定义两个具有相同名称的通用函数。通常情况并非如此;例如,Clojure multimethods明确属于命名空间,因此如果你有两个这样的通用函数具有相同的名称,你需要明确你使用的是什么。

简而言之,“概念上不同”的函数将始终具有不同的名称,或者位于不同的名称空间中。

答案 2 :(得分:3)

通用函数应对其实现的方法执行相同的“动词”。

在动物/树“树皮”案例中,动物动词是“执行声音动作”,在树案例中,嗯,我猜它是制作环境盾牌。

英语碰巧称他们为“吠叫”只是一种语言共同发生。

如果您遇到多个不同的GF(通用函数)确实应该具有相同名称的情况,使用命名空间来分隔它们(可能)是正确的。

答案 3 :(得分:2)

通常,消息传递OO不能解决您所谈论的命名空间问题。具有结构类型系统的OO语言不区分bark中的方法AnimalTree中的方法,只要它们具有相同的类型即可。这只是因为流行的OO语言使用名义类型的系统(例如Java)。

答案 4 :(得分:2)

  

因为XYZ的get-x()与GENE的get-x()没有概念上的关系,   它们作为单独的通用函数实现

不确定。但是因为他们的arglist是相同的(只是将对象传递给方法),所以你可以在同一个泛型函数上将它们实现为不同的方法。

将方法添加到泛型函数时唯一的约束是方法的arglist与泛型函数的arglist匹配。

  

更一般地说,方法必须具有相同数量的所需和   可选参数,必须能够接受任何参数   对应于通用指定的任何&amp; rest或&amp; key参数   功能

没有约束,功能必须在概念上相关。大部分时间他们(超越超类等),但他们当然不必。

虽然这个约束(需要相同的arglist)似乎有时会受到限制。如果你看Erlang,函数有arity,你可以定义多个具有不同arity的相同名称的函数(具有相同名称和不同arglists的函数)。然后一种调度负责调用正确的函数。我喜欢这个。在lisp中,我认为这将映射到具有通用函数接受具有不同arglists的方法。也许这是可以在MOP中配置的东西?

虽然阅读更多here,但似乎关键字参数可能允许程序员通过在不同方法中使用不同的键来改变其参数数量来实现具有完全不同arity的方法的泛型函数:

  

一种方法可以“接受”其通用中定义的&amp; key和&amp; rest参数   通过具有&amp; rest参数,具有相同的&amp;密钥来起作用   参数,或通过指定&amp; allow-other-keys和&amp;键。一个   方法还可以指定在通用中找不到的&amp; key参数   函数的参数列表 - 当调用泛型函数时,任何   &amp;通用函数指定的关键参数或任何适用的参数   方法将被接受。

另请注意,这种模糊,其中存储在通用函数中的不同方法在概念上是不同的事情,发生在你的'树有树皮'的幕后,'狗吠'的例子。定义树类时,您需要为树皮槽设置自动getter和setter方法。在定义dog类时,你要在dog类型上定义一个实际进行吠​​叫的bark方法。这两种方法都存储在#'srab泛型函数中。

由于它们都包含在相同的泛型函数中,因此您将以完全相同的方式调用它们:

(bark tree-obj) -> Returns a noun (the bark of the tree)
(bark dog-obj) -> Produces a verb (the dog barks)

代码:

CL-USER> 
(defclass tree ()
  ((bark :accessor bark :initarg :bark :initform 'cracked)))
#<STANDARD-CLASS TREE>
CL-USER> 
(symbol-function 'bark)
#<STANDARD-GENERIC-FUNCTION BARK (1)>
CL-USER> 
(defclass dog ()
  ())
#<STANDARD-CLASS DOG>
CL-USER> 
(defmethod bark ((obj dog))
  'rough)
#<STANDARD-METHOD BARK (DOG) {1005494691}>
CL-USER> 
(symbol-function 'bark)
#<STANDARD-GENERIC-FUNCTION BARK (2)>
CL-USER> 
(bark (make-instance 'tree))
CRACKED
CL-USER> 
(bark (make-instance 'dog))
ROUGH
CL-USER> 

我倾向于支持这种“语法的二元性”,或者特征的模糊等等。我不认为泛型函数上的所有方法都必须在概念上相似。这只是一个指导IMO。如果英语中的语言交互发生(作为名词和动词吠叫),那么拥有一种优雅地处理案例的编程语言是很好的。

答案 5 :(得分:0)

您正在处理几个概念,并将它们混合在一起,例如:名称空间,全局泛型函数,本地泛型函数(方法),方法调用,消息传递等。

在某些情况下,这些概念可能会在理论上重叠,难以实施。在我看来,你在脑海里也混合了很多概念。

功能语言,不是我的力量,我已经与LISP做了一些工作。

但是,其中一些概念用于其他范例,例如Procedural,&amp;对象(类)方向。您可能想要检查这些概念是如何实现的,以后再回到您自己的编程语言中。

例如,我认为非常重要的是使用命名空间(“模块”),作为程序编程的单独概念,并避免标识符冲突,就像你提到的那样。像你这样的命名空间的编程语言就像这样:

===在文件animal.code ===

define module animals

define class animal
  // methods doesn't use "bark(animal AANIMAL)"
  define method bark()
  ...
  end define method
end define class

define class dog
  // methods doesn't use "bark(dog ADOG)"
  define method bark()
  ...
  end define method
end define class

end define module
文件myprogram.code ===

中的

===

define module myprogram

import animals.code
import trees.code

define function main
  a = new-dog()
  a.bark() //Make the dog bark

  …

  t = new-tree()
  b = t.bark() //Retrieve the bark from the tree
end define function main

end define module

干杯。

答案 6 :(得分:0)

这是一个普遍的问题,即许多编程语言试图以方便的方式解决调度表的位置。

在OOP的情况下,我们将它放入类定义中(我们以这种方式使用类型+函数具体化,加上继承,它给出了架构问题的所有乐趣)。

在FP的情况下,我们把它放在调度函数中(我们有一个共享的集中表,这通常不是那么糟糕,但也不完美)。

我喜欢基于接口的方法,当我可以分别创建任何数据类型和任何共享函数定义(Clojure中的协议)的虚拟表时。

在Java中(对不起),它看起来像这样:

我们假设ResponseBody是一个接口。

public static ResponseBody create(MediaType contentType,
     long contentLength, InputStream content) {

    return new ResponseBody() {
      public MediaType contentType() {
        return contentType;
      }

      public long contentLength() {
        return contentLength;
      }

      public BufferedSource source() {
        return streamBuffered(content);
      }
    };
}

为此特定create函数创建虚拟表。这完全解决了命名空间问题,如果你想要,你也可以有一个非集中式的基于类型的调度(OOP)

在没有为测试目的声明新数据类型的情况下,单独实现也变得微不足道。