Why does add: return the object added in Smalltalk collections?
Asked Answered
B

5

10

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?
Benzofuran answered 27/12, 2012 at 0:24 Comment(2)
Note that if you have a refactoring browser (which is the case by default in Pharo 2.0 and VW too), there is a lint rule (Code critics) that checks whether the value returned by #add: is used or not.Zug
There's no good reason. The best I can come up with is that it blocks the Lisp style of lots of embedded expressions and enforces a much more procedural look with cascades. Perhaps the designers were so worried that it would turn into Lisp that they deliberately short-circuited it.Magus
G
13

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.

Glomma answered 27/12, 2012 at 7:52 Comment(1)
I remember I didn't understand the point of #yourself the first time I browsed its implementation, and it's like it is here to ruin the simplicity of above snippet. The real question is whether this teaches something useful or not. The great majority of #yourself usage seems dedicated against add: remove: at:put:Zug
Z
7

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).
Zug answered 27/12, 2012 at 1:22 Comment(0)
P
3

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.

Parkins answered 27/12, 2012 at 8:12 Comment(1)
Ah yes, the worse thing is to have heterogeneous conventions across library for the same message. In Squeak and Pharo we recently tried to restore some homogeneity and always return the added/removed/put object.Zug
M
2

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
Mom answered 27/12, 2012 at 20:27 Comment(0)
W
0

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.

Wikiup answered 5/1, 2013 at 1:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.