微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在 Smalltalk 中,当发送者和参数是不同类型时,定义可交换二进制方法的最佳方法是什么?

如何解决在 Smalltalk 中,当发送者和参数是不同类型时,定义可交换二进制方法的最佳方法是什么?

假设您有一个 Foo 类,并且您希望能够将一个 Foo 乘以一个 Number 以获得另一个 Foo,使用“@@”作为乘号。

因为乘法是可交换的,所以能够写出就好了:

| f a b |
f := Foo new.
a := 3 @@ f.
b := f @@ 3.
self assert: a = b

这不仅需要将二进制方法“@@”添加到 Foo 中,还需要添加到 Number 类中。所以你最终在两个不同的地方使用了本质上相同的方法(以及循环依赖),这看起来很不优雅。

所以我想知道,在 Smalltalk 中,有没有其他方法可以创建可交换的二进制方法,其中发送者和参数属于不同类型——一种不需要您在两个不同的类中定义相同消息的方法

如果不是,是否可以使用 Smalltalk 本身来创建这种能力(即,添加自动管理可交换二进制方法的类/方法,而无需更改实际的 Smalltalk 语言或 VM)?

解决方法

在您的情况下,如果您没有将 @@ 消息的参数发送到 Foo 的实例,会发生什么情况也值得质疑。

例如:

f @@ 'hello'

要省略这一点,您可以使用双重分派。所以你定义了一个乘以一个数的方法:

Foo>>#multiplyWithANumber: aNumber
    "do multiplication with a number"

然后在对象层次结构中,定义@@的入口点

Object>>#@@ aFoo
    "signal some error saying that this operation is not supported"
    self shouldNotImplement

Number>>#@@ aFoo
    ^ aFoo multiplyWithANumber: self

Foo>>#@@ anObject
    "pass decision to the parameter"
    "also,what should happen if anObject is a Foo"
    ^ anObject @@ self
    

这可能过于复杂,在一个简单的情况下,如果您不太关心类型,并且想避免重复,您可以:

Foo>>#multiplyWithANumber: aNumber
    "do multiplication with a number"

Foo>>#@@ aNumber
    ^ self multiplyWithANumber: aNumber

Number>>#@@ aFoo
    ^ aFoo multiplyWithANumber: self

当然,您可以将 multiplyWithANumber: 全部跳过,只用一个 @@ 实现(可能在 Foo 方面,因为它是此实现的主要原因),另一个 {{1} } 只调用 @@ 和实现。我喜欢有一个详细的方法,所以很清楚发生了什么,你不必写额外的评论。

,

在 Smalltalk 中,规则很简单:消息由接收者解释,然后在接收者类中查找方法,然后是超类。

在二进制消息的情况下,如果我们想根据接收器和参数类型分派到特定方法,那么众所周知的模式是使用双分派,如 Uko 的回答所示。

Foo>>op: b
    ^b opFromFoo: self

Bar>>op: b
    ^b opFromBar: self

现在的问题是您可能有两个相同数学的实现。操作:

Foo>>opFromBar: b
    "operate on a Foo and a Bar"
    ...snip...

Bar>>opFromFoo: b
    "operate on a Bar and a Foo"
    ...snip...

可能的解决方法 1:既然您知道 op: 是可交换的,那么让一个分派给另一个:

Foo>>opFromBar: b
    "op: is commutative,let Bar do the job"
    ^b opFromFoo: self

Bar>>opFromFoo: b
    "operate on a Bar and a Foo - do the real work"
    ...snip...

您不必复制核心,但仍然需要为 n 个不同类型定义 n*n 个调度方法...

解决方法 2:在自己的类中具体化操作

OpAlgo>>opFoo: a andBar: b
    "perform op with a Foo and a Bar"

那仍然需要双重调度(n*n 个方法),并且可能会泄露 Foo Bar 的内部实现细节,而且 OpAlgo 是一种没有真实状态的实用程序。

解决方法 3:在 Smalltalk 中实现多分派。在这里设计解决方案的时间太长了,但您会在网上找到参考资料,例如 http://www.laputan.org/reflection/Foote-Johnson-Noble-ECOOP-2005.html

,

从某种简单的意义上说,您所问的问题在面向对象中根本不可能。注意:不仅在 Smalltalk 中,而且在面向对象中一般

OO 的基本思想是所有计算都是通过对象向对象发送消息来进行的。 消息的接收者可以完全控制如何响应,这是消息传递的基本性质。

所以,这意味着在 a @@ b 中,a 可以完全控制如何响应 @@,而在 b @@ a 中,它是 {{1 }} 可以完全控制如何响应。作为 Smalltalk 的一部分,或者在一般的面向对象中,没有任何机制来确保答案是相同的。

请注意,这确实适用即使 ba 属于同一类型。 (无论如何,Smalltalk 的术语“类型”定义不明确。)

面向对象中的对象具有威廉库克称之为autognosis(自知)的属性,他的意思是对象只知道关于它们自己。例如,一个对象不能观察或操作另一个对象的表示或状态,或者访问其他对象的内部 API。它只能发送公共消息并观察响应。

这使得对象与抽象数据类型的实例根本不同,其中相同类型的实例可以检查和操作彼此的表示,并访问彼此的内部 API。这意味着,例如,在 Java 中,类的实例不是对象。它们是抽象数据类型的实例。只有接口的实例才是对象。

出于这个原因,我个人更喜欢术语xeno-agnosis,意思是“不了解他人”,因为重点不是物体了解自己,而是它们了解了解自己,不了解他人。

所有这些只是一种冗长的说法,即在面向对象中,总是一个对象,即接收者,它决定如何响应消息。这意味着,从根本上不可能保证可交换性。 (除了接收者和参数是同一个对象的微不足道的情况,即 b。)

你只有两个选择:

  1. 创建一个第三对象,在这两种情况下都用作单个接收者。
  2. 这两个对象必须协作彼此,并且您必须相信它们不会破坏彼此的合同 - 无法执行此合同。

案例#1 是某种上下文对象,可能代表各种代数。有一些语言对此有语法支持,例如Ioke 和Seph 都有他们所谓的三元运算符,它们编写为二元运算符,但实际上是三元运算符,实际接收器是默认的隐式接收器。 (a @@ a 在 Smalltalk 中,在 Ioke 和 Seph 中,它被称为当前基础。)这就是 Ioke 和 Seph 支持甚至赋值作为可重载运算符的方式:

self

实际上相当于

foo = bar

这使您可以重新定义 DSL 中赋值的含义,这真的很酷,而且还使语言更加规范,因为赋值没有什么特别之处:它只是像其他消息一样发送消息。

所以,这是您的选项 1:定义一个上下文对象,在其中计算乘法,并且这个上下文对象知道如何处理 (Number,Foo) 以及 (Foo,Number)。像这样:

=(foo,bar)

如果您确定只有一个可能的“代数”,这可以是 FooAlgebra>>#multiply: aFooOrNumber with: anotherFooOrNumber "multiplies Foos and Numbers commutatively" (aFooOrNumber isKindOf: Foo) ifTrue: [] ifFalse: [] 的类方法,也可以是 Foo 对象的实例方法,如果您希望有是多个不同参数化的代数,或者它甚至可以是具有多个实现的抽象协议,如果您希望有多个具有不同行为的代数。

选项#2 是设计某种策略,使两个对象如何相互协作以尝试给出相同的响应。这样做的一种方法实际上是让两个对象委托给第三个对象,即使用选项 #1 实现选项 #2。

某种形式的双重调度是解决此问题的典型方法,已在其他答案中进行了演示。

在 Smalltalk 的数字层次结构中解决这个问题的方法实际上是通过将操作数转换为 FooAlgebra 并且在 Float 类中只有一个操作实现来执行大多数操作。

在 Ruby 中,有一个更通用的数字强制转换协议,其中任何不知道如何处理操作数的“类似数字”的对象都会让该操作数有机会对一个对象执行强制转换是否知道如何执行操作。

在您的示例中,如果使用了类似 Ruby 的强制协议,则意味着您可以创建新的类似数字的对象而无需修改任何现有的类似数字的类仍然可以干净地与它们交互!

以下是 Ruby 强制协议如何工作的示例:

Float

您将相应地实施您的 class Integer # the built-in classes would be implemented something like this: def *(other) case other when Integer # I know how to do this! when Float # I'll just convert myself to `Float` and let `Float` handle it,since multiplication is commutative: other * to_f else # Don't really know what to do,so I ask the other guy: coerced_self,coerced_other = other.coerce(self) coerced_self * coerced_other end end

Foo

现在,当您调用 class Foo def *(other) case other when Foo # I know how to do this when Integer # Well,I know how to multiply `Foo`s and I know how to create them: self * Foo.new(other) else # Don't really know what to do,coerced_other = other.coerce(self) coerced_self * coerced_other end def coerce(other) return Foo.new(other),self end end 时,3 * f 会说“我实际上不知道该怎么做”,并会要求 3 转换对 {{1} } 变成知道如何处理 f 的东西。 [3,f] 将用 f 对进行响应,然后 f 将重试该操作,但是现在该操作将被分派到 [Foo.new(3),f],后者知道如何处理 {{1} }s.

当然,您可以在 Smalltalk 中做同样的事情。这将是更多的前期工作,因为 3 方法尚不存在,因此您必须将其添加到所有现有的类似数字的类中。并且已经存在的数字运算也没有使用这个强制协议,所以你也必须修补所有这些 - 但无论如何你想添加一个新的运算,所以不需要。

这本质上是其他两个答案的扩展版本,以防您想添加几个新操作而不必为每个操作复制双调度逻辑。这基本上是双调度思想的扩展,具有两层调度。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。