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

Viewed 181

Suppose you have a class Foo, and that you want to be able to multiply a Foo by a Number to get another Foo, using ‘@@‘ as the multiplication sign.

Since multiplication is commutative, it would be nice to be able to write:

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

This requires not just adding the binary method “@@” to Foo, but also to the Number class. So you end up with essentially the same method in two different places (along with a circular dependency), which seems rather inelegant.

So I'm wondering, in Smalltalk, is there any other way to create commutative binary methods where the sender and argument are of different types - one which doesn’t require you to define the same message in two different classes?

If not, is possible to create this ability using Smalltalk itself (ie. add classes/methods which automate the management of commutative binary methods, without changing the actual Smalltalk language or VM)?

3 Answers

In your case it's also worth questioning what happens if you send not a number the parameter of the @@ message to an instance of Foo.

E.g:

f @@ 'hello'

To omit that, you can use double dispatch. So you define a method that multiplies a number:

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

Then in the object hierarchy, you define the entry points of @@

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
    

This may be overcomplicated, you in a simple case if you care less about the types, and want to avoid duplication, you can have:

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

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

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

Of course you can skip multiplyWithANumber: all-together and just have one @@ with implementation (probably on the Foo side, because it's the main reason of this implementation), and another @@ which just calls the @@ with implementation. I like to have a verbose method so it's clear what is going on and you don't have to write additional comments.

In Smalltalk, the rules are simple: the message is interpreted by the reeiver, and a method is looked-up in receiver class, then superclasses.

In case of binary message, if we want to dispatch to a specific method depending on both receiver and argument types, then a well known pattern is to use double-dispatching as demonstrated in Uko's answer.

Foo>>op: b
    ^b opFromFoo: self

Bar>>op: b
    ^b opFromBar: self

The problem is now that you may have two implementations of the same math. operation:

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

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

Possible workarounds 1: since you know that op: is commutative, let one dispatch to the other:

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...

You don't have to duplicate the core, but still have to define n*n dispatch methods for n different types...

Workaround 2: reify the operation in its own class

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

That still require double dispatching (n*n methods), and may leak internal implementations detail of Foo Bar, plus OpAlgo is kind of a utility with no real state.

Workaround 3: implement a multiple-dispatch in Smalltalk. It would be too long to devise a solution here, but you'll find references on the net, like http://www.laputan.org/reflection/Foote-Johnson-Noble-ECOOP-2005.html for example

In some simple sense, what you are asking about is fundamentally impossible in Object-Orientation. Note: not just in Smalltalk, but in Object-Orientation in general.

The fundamental idea of OO is that all computation happens by objects sending messages to objects. It is in the fundamental nature of messaging that the receiver of the message has full control over how to respond.

So, that means that in a @@ b, it is a which has full control over how to respond to @@, whereas in b @@ a, it is b which has full control over how to respond. There is no mechanism as part of Smalltalk in particular or in OO in general to ensure that the answers will be the same.

Note that this does apply even if a and b are of the same type. (Where the term "type" for Smalltalk is ill-defined anyway.)

Objects in OO have a property that William Cook calls autognosis (self-knowledge), by which he means that objects know only about themselves. For example, an object cannot observe or manipulate another object's representation or state, or access other object's internal APIs. It can only send public messages and observe the responses.

This makes Objects fundamentally different from instances of Abstract Data Types, where instances of the same type can inspect and manipulate each other's representation, and access each other's internal APIs. This means, for example, that in Java, instances of classes are not Objects. They are instances of Abstract Data Types. Only instances of interfaces are Objects.

For this reason, I personally prefer the term xeno-agnosis, meaning "non-knowledge about others", because the salient point is not that objects know about themselves, but that they know only about themselves, and have no knowledge about others.

All of this is just a long-winded way of saying that in OO, there is always one single object, namely the receiver, which decides how to respond to a message. Which means, it is fundamentally impossible to guarantee commutativity. (Except for the trivial case where the receiver and the argument are one and the same object, i.e. a @@ a.)

You only have two choices:

  1. Create a third object that is used as the single receiver in both cases.
  2. The two objects must collaborate with each other, and you must trust that they don't break their contract with each other – there is no way to enforce this contract.

Case #1 would be some sort of a context object, maybe representing an algebra of sorts. There are some languages which have syntactic support for this, e.g. both Ioke and Seph have what they call trinary operators, which are written as binary operators, but are actually ternary operators with the actual receiver being the default implicit receiver. (self in Smalltalk, in Ioke and Seph, it is called the current ground.) That is how Ioke and Seph support even assignment as an overloadable operator:

foo = bar

is actually equivalent to

=(foo, bar)

This allows you to redefine the meaning of assignment in DSLs, which is really cool, and it also makes the language even more regular, since assignment is nothing special: it's just a message send like any other.

So, this is your option 1: define a context object within which this multiplication is evaluated, and this context object knows how to deal with (Number, Foo) as well as (Foo, Number). Something like this:

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

This could either be a class method of Foo if you are sure that there is only one possible "algebra", it could be an instance method of a FooAlgebra object, if you expect there to be multiple differently parameterized algebras, or it could even be an abstract protocol with multiple implementations, if you expect there to be multiple algebras with different behaviors.

Option #2 is to devise some sort of strategy how the two objects can collaborate with each other to try and give the same response. One way of doing this is actually having both objects delegate to a third object, i.e. implement option #2 using option #1.

Some form of double-dispatch is the typical way to solve this, already demonstrated in other answers.

The way this is solved in Smalltalk's numeric hierarchy is actually that most operations are carried out by converting the operands to Floats and having only a single implementation of the operations in the Float class.

In Ruby, there is a more general numeric coercion protocol, where any "number-like" object that doesn't know how to deal with an operand, will give that operand the opportunity to perform coercion to an object that does know how to perform the operation.

In your example, if a Ruby-like coercion protocol were used, that would mean that you can create your new number-like object without having to modify any existing number-like class and it would still interoperate with them cleanly!

Here's an example for how the Ruby coercion protocol works:

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

And you would implement your Foo accordingly:

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

Now, when you call 3 * f, 3 will say "I don't actually know how to do this", and will ask f to convert the pair [3, f] into something that knows how to do deal with f. f will respond with the pair [Foo.new(3), f], then 3 will retry the operation, which will however now be dispatched to Foo#*, which knows how to deal with Foos.

You can do the same in Smalltalk, of course. It will be a little more up-front work, because the coerce method does not yet exist, so you will have to add it to all existing number-like classes. And the already existing numeric operations also do not make use of this coercion protocol, so you would have to patch up all of those as well – but you want to add a new operation anyway, so that is not needed.

This is essentially an expanded version of the other two answers, in case you want to add several new operations without having to replicate the double-dispatch logic for each of the operations. This is basically an extension of the double-dispatch idea with two layers of dispatch.

Related