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

Pat*_* Li 32 lisp language-design clojure common-lisp 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.
Run Code Online (Sandbox Code Playgroud)

===在文件myprogram.code ===

import animal.code
for each animal a in list-of-animals :
   a.bark()
Run Code Online (Sandbox Code Playgroud)

在这个问题中,"树皮"是一种具有多个"分支"的方法,它们根据参数类型的不同而不同.我们对我们感兴趣的每种参数类型(狗和猫)实施一次"树皮".在运行时,我们能够遍历动物列表并动态选择要采用的分支.

(2)它解决了命名空间问题:

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

   - Animals can "bark"
Run Code Online (Sandbox Code Playgroud)

===在文件tree.code ===

   - Trees have "bark"
Run Code Online (Sandbox Code Playgroud)

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

在这个问题上,"汪汪"的其实是两个概念上不同的功能,只是碰巧有相同的名称.参数的类型(无论是狗还是树)决定了我们实际意味着什么功能.


多方法优雅地解决问题编号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")
Run Code Online (Sandbox Code Playgroud)

===在文件myprogram.code ===

import animal.code
for each animal a in list-of-animals :
   bark(a)
Run Code Online (Sandbox Code Playgroud)

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

(2)为什么多方法不适用于动物和树木

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

   - define generic function bark(Animal a)
Run Code Online (Sandbox Code Playgroud)

===在文件tree.code ===

   - define generic function bark(Tree t)
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


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

我的具体问题是在需要混淆的情况下.

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

=== file:XYZ.code ===

define class XYZ :
   define get-x ()
   define get-y ()
   define get-z ()
Run Code Online (Sandbox Code Playgroud)

=== file:POINT.code ===

define class POINT :
   define get-x ()
   define get-y ()
Run Code Online (Sandbox Code Playgroud)

=== file:GENE.code ===

define class GENE :
   define get-x ()
   define get-xx ()
   define get-y ()
   define get-xy ()
Run Code Online (Sandbox Code Playgroud)

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

由于命名空间解析与调度的混合,程序员可以天真地在所有三个对象上调用get-x().这也是完全明确的.每个对象"拥有"它自己的一组方法,因此对于程序员的意思没有混淆.

与多方法版本对比:


=== file:XYZ.code ===

define generic function get-x (XYZ)
define generic function get-y (XYZ)
define generic function get-z (XYZ)
Run Code Online (Sandbox Code Playgroud)

=== file:POINT.code ===

define generic function get-x (POINT)
define generic function get-y (POINT)
Run Code Online (Sandbox Code Playgroud)

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

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

因为XYZ的get-x()与GENE的get-x()没有概念上的关系,所以它们被实现为单独的泛型函数.因此,最终程序员(在my_program.code中)必须明确限定get-x()并告诉系统他实际上要调用哪个 get-x().

确实,这种显式方法更清晰,易于推广到多个调度和多重继承.但是使用(滥用)调度来解决命名空间问题是消息传递OO的一个非常方便的特性.

我个人认为98%的自己的代码使用单调度和单继承充分表达.我使用dispatch进行命名空间解析比使用多次调度更方便,所以我不愿意放弃它.

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


似乎达成了共识

  • multimethods解决调度问题但不攻击命名空间问题.
  • 概念上不同的函数应该具有不同的名称,并且应该期望用户手动限定它们.

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

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

我喜欢泛型函数的概念,但目前认为它们是为了制造"非常困难的东西而不是那么难"而牺牲了"琐碎的事情有点烦人".由于大多数代码都是微不足道的,我仍然认为这是一个值得解决的问题.

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

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

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

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

(animal:bark some-animal)

(game-animal:bark some-game-animal)
Run Code Online (Sandbox Code Playgroud)

符号具有语法

PACKAGE-NAME::SYMBOL-NAME
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

(defmethod bite ((some-animal dog) (some-food bone))
  ...)
Run Code Online (Sandbox Code Playgroud)

上面使用的类CAT,HUMAN,DOGBONE.泛型函数应属于哪个类?特殊命名空间会是什么样的?

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

动机:

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

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

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

什么时候

(defmethod bark ((some-animal dog)) ...)
(defmethod bark ((some-tree oak)) ...)
Run Code Online (Sandbox Code Playgroud)

看起来很相似,它们是两个概念上不同的动作.

还有一点:

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

(defmethod bark ((some-tree oak)) ...)
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

还有其他语言与Lisp/CLOS更不同,并且尝试(ed)支持多方法:

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


san*_*inc 9

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

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