Question

Background

Something that catches every Smalltalk newbie is that add: does not return "self" but the object being added.

For example, with this code:

myCollection := OrderedCollection new 
  add: 'Peter';
  add: 'John';
  add: 'Paul'.

myCollectionwill containt the String "Paul", and not the collection itself.

This is because add: returns the object being added, and the whole cascade expression evaluates to the last message being sent.

Instead, it should be written with yourself at the end:

myCollection := OrderedCollection new 
  add: 'Peter';
  add: 'John';
  add: 'Paul';
  yourself.

Questions

  • Why is this so?
  • What was this designed this way?
  • What are the benefits of add: behaving this way?
Was it helpful?

Solution

I've thought about this a lot. I've never heard any of the original designers of Smalltalk defend this decision, so we don't know for sure why they did it. I've decided that the reason was because of cascades. If add: returned the receiver, then (things add: thing1) add: thing2 would be the same as things add: thing1; add: thing2. By having add: return the argument, those two expressions are different and the programmer can use each when it is appropriate.

However, I think it is a mistake. I've been teaching Smalltalk for over 25 years, and every time I teach it, people have trouble with this. I always warn them, but they still make mistakes with add:. So, I consider this a bad design decision.

This design decision is about the library, not the compiler. You could change it by going into the collection classes and changing them. Of course, it is impossible to predict how many Smalltalk programs would break. Collections are so fundamental that this change would be as hard to make as a real change to the language.

OTHER TIPS

In other languages you can write:

b[j] = a[i] = e;

This is somehow preserved in Smalltalk if at:put: returns the put object:

collectionB at: j put: (collectionA at: i put: e).

The same interest exist for add: / remove: which allow this kind of chaining:

collectionB add: (collectionA add: anElement).
collectionB add: (collectionA remove: anElement).

it's always best to cascade such method-sends and never ever rely on their return values. It's the same with setter methods, some times they may return self, some times they return the parameter. It's safest to assume that the return value of these methods is close to random and never ever use it.

I can't defend it, and I can't refute Ralph's experience either.

A desire for symmetry may have been a contributing factor. Given that #remove: returns the object that was removed, it makes some sense to have #add: return the object that is added.

Simple examples bias us, I think, as well. When we have the object to add in a variable already, or it's a simple literal, the value of the return seems pointless. But if we have (questionable) code that looks like this:

someProfile add: VirtualMachine youngSpaceEnd - VirtualMachine oldSpaceEnd

If someProfile is a linear list, I supposed you can fetch the value you just add:'ed via last. But it might just be a Bag, or a Set. In that case, it can be handy to do:

currentSize := someProfile add: VirtualMachine youngSpaceEnd - VirtualMachine oldSpaceEnd

Some would consider that better than:

someProfile add: (currentSize := VirtualMachine youngSpaceEnd - VirtualMachine oldSpaceEnd)

Though the best would be:

currentSize := VirtualMachine youngSpaceEnd - VirtualMachine oldSpaceEnd.
someProfile add: currentSize

The best explanation I've come up with is to make it equivalent to an assignment. Suppose you have code like this:

(stream := WriteStream on: String new) nextPutAll: 'hello'.
stream nextPut: $!

Assigning into a variable evaluates to the object assigned. I should be able to replace the variable with a collection and get equivalent behavior:

(array at: 1 put: (WriteStream on: String new)) nextPutAll: 'hello'.
(array at: 1) nextPut: $!

Now, having said that, I would chastise any developer who wrote this code because it's unreadable. I'd rather separate it into two lines:

array at: 1 put: (WriteStream on: String new).
array first
   nextPutAll: 'hello';
   nextPut: $!

This is the best justification I can give. It's done to make assignment into collections consistent with assignment into variables but if you take advantage of that feature you have code that's hard to read.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top