簡體   English   中英

在 Smalltalk 中,當發送者和參數屬於不同類型時,定義可交換二進制方法的最佳方法是什么?

[英]In Smalltalk, what’s the best way of defining a commutative binary method when the sender and argument are of different types?

假設您有一個 class Foo,並且您希望能夠將一個 Foo 乘以一個 Number 以獲得另一個 Foo,使用“@@”作為乘號。

由於乘法是可交換的,因此能夠寫成:

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

這不僅需要將二進制方法“@@”添加到 Foo,還需要添加到數字 class。 因此,您最終會在兩個不同的地方使用基本相同的方法(以及循環依賴),這似乎相當不雅。

所以我想知道,在 Smalltalk 中,是否有任何其他方法可以創建可交換二進制方法,其中發送者和參數是不同類型的 - 一種不需要您在兩個不同類中定義相同消息的方法?

如果沒有,是否可以使用 Smalltalk 本身來創建此功能(即添加自動管理可交換二進制方法的類/方法,而不更改實際的 Smalltalk 語言或 VM)?

在您的情況下,如果您將@@消息的參數不是數字發送到Foo的實例,那么也值得質疑會發生什么。

例如:

f @@ 'hello'

要省略它,您可以使用雙重調度。 因此,您定義了一個乘以數字的方法:

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

然后在 object 層次結構中,定義@@的入口點

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: all-together 並且只有一個@@ with implementation(可能在 Foo 方面,因為它是這個實現的主要原因),另一個@@只是調用@@ with implementation。 我喜歡有一個詳細的方法,所以很清楚發生了什么,你不必寫額外的評論。

在 Smalltalk 中,規則很簡單:消息由接收方解釋,並在接收方 class 中查找方法,然后查找超類。

在二進制消息的情況下,如果我們想根據接收者和參數類型分派到特定的方法,那么一個眾所周知的模式是使用雙重分派,如 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:在自己的 class 中具體化操作

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

這仍然需要雙重調度(n*n 方法),並且可能會泄漏 Foo Bar 的內部實現細節,而且 OpAlgo 是一種沒有真正 state 的實用程序。

解決方法 3:在 Smalltalk 中實現多分派。 devise 在這里找到解決方案太長了,但是您會在網上找到參考資料,例如http://www.laputan.org/reflection/Foote-Johnson-Noble-ECOOP-2005.ZFC3569ZA883示例

從某種簡單的意義上說,您所問的問題在面向對象中根本不可能 注意:不僅在 Smalltalk 中,而且在一般的面向對象中。

OO 的基本思想是所有計算都是通過對象向對象發送消息來進行的。 消息的接收者完全控制如何響應是消息傳遞的基本性質。

因此,這意味着在a @@ b中, a可以完全控制如何響應@@ ,而在b @@ a中, b可以完全控制如何響應。 作為 Smalltalk 的一部分,或者在一般的 OO 中,沒有任何機制可以確保答案是相同的。

請注意,即使ab屬於同一類型,這也適用。 (無論如何,Smalltalk 的術語“類型”定義不明確。)

OO 中的對象具有 William Cook 稱為自知(self-knowledge)的屬性,他的意思是對象知道自己。 例如,object無法觀察或操作另一個對象的表示或 state,或訪問其他對象的內部 API。 只能發送公共消息並觀察響應。

這使得對象從根本上不同於抽象數據類型的實例,在抽象數據類型的實例中,相同類型的實例可以檢查和操作彼此的表示,並訪問彼此的內部 API。 這意味着,例如,在 Java 中,類的實例不是 Objects 它們是抽象數據類型的實例。 只有接口的實例是對象。

出於這個原因,我個人更喜歡xeno-agnosis 一詞,意思是“不知道他人”,因為重點不是對象知道自己,而是他們知道自己,而對他人一無所知。

所有這些都只是一種啰嗦的說法,在 OO 中,總是有一個單獨的 object ,即接收者,它決定如何響應消息。 這意味着,從根本上不可能保證可交換性。 (除了接收者和參數是一個且相同的 object,即a @@ a的瑣碎情況。)

你只有兩個選擇:

  1. 創建第三個object,在這兩種情況下都用作單個接收器。
  2. 這兩個對象必須相互協作,並且您必須相信它們不會破壞彼此之間的合同——沒有辦法強制執行這個合同。

案例#1 是某種上下文 object,可能代表某種代數。 有一些語言對此有語法支持,例如 Ioke 和 Seph 都有他們所謂的三元運算符,它們被寫成二元運算符,但實際上是三元運算符,實際接收者是默認的隱式接收者。 (在 Smalltalk 中self ,在 Ioke 和 Seph 中,它被稱為current ground 。)這就是 Ioke 和 Seph 支持偶數賦值作為可重載運算符的方式:

foo = bar

實際上相當於

=(foo, bar)

這允許您重新定義 DSL 中賦值的含義,這真的很酷,而且它也使語言更加規則,因為賦值沒有什么特別的:它只是一個消息發送和其他任何東西一樣。

因此,這是您的選項 1:定義一個上下文 object 來評估此乘法,並且此上下文 object 知道如何處理 (Number, Foo) 以及 (Foo, Number)。 像這樣的東西:

FooAlgebra>>#multiply: aFooOrNumber with: anotherFooOrNumber
    "multiplies Foos and Numbers commutatively"
    (aFooOrNumber isKindOf: Foo) ifTrue: [] ifFalse: []

如果您確定只有一個可能的“代數”,這可能是FooFooAlgebra方法,如果您希望有多個不同參數化的代數,它可能是 FooAlgebra object 的實例方法,或者它甚至可以是如果您希望有多個具有不同行為的代數,則具有多個實現的抽象協議。

選項 #2 是 devise 某種策略,兩個對象如何相互協作以嘗試給出相同的響應。 這樣做的一種方法實際上是讓兩個對象都委托給第三個 object,即使用選項 #1 實現選項 #2。

某種形式的雙重調度是解決這個問題的典型方法,已經在其他答案中證明了這一點。

在 Smalltalk 的數字層次結構中解決這個問題的方法實際上是通過將操作數轉換為Float來執行大多數操作,並且在Float class 中只有一個操作實現。

在 Ruby 中,有一個更通用的數字強制協議,其中任何不知道如何處理操作數的“類似數字” object 將使該操作數有機會對知道如何處理的 ZA8CFDE63311BD59EB2ACZ6F966 執行強制操作。

在您的示例中,如果使用了類似 Ruby 的強制協議,這意味着您可以創建新的類似數字的 object而無需修改任何現有的類似數字的 class並且它仍然可以干凈地與它們互操作!

下面是 Ruby 強制協議如何工作的示例:

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, so I ask the other guy:
      coerced_self, coerced_other = other.coerce(self)
      coerced_self * coerced_other
  end

  def coerce(other)
    return Foo.new(other), self
  end
end

現在,當您調用3 * f時, 3會說“我實際上不知道該怎么做”,並要求f[3, f]對轉換為知道如何處理f的東西。 f將以對[Foo.new(3), f]進行響應,然后3將重試該操作,但是現在將分派到Foo#* ,它知道如何處理Foo

當然,您可以在 Smalltalk 中做同樣的事情。 這將是更多的前期工作,因為coerce方法尚不存在,因此您必須將其添加到所有現有的類似數字的類中。 並且已經存在的數字操作也沒有使用這個強制協議,所以你也必須修補所有這些——但無論如何你都想添加一個新的操作,所以這不是必需的。

這本質上是其他兩個答案的擴展版本,以防您想添加幾個新操作而不必為每個操作復制雙重調度邏輯。 這基本上是具有兩層調度的雙重調度思想的擴展。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM