Set>>add:

Richard A. O'Keefe ok at cs.otago.ac.nz
Wed Nov 13 22:52:25 UTC 2002


Nevin Pratt <nevin at smalltalkpro.com> wrote:
	Just playing the devil's advocate here...
	
	Why is 'aSet add: nil' wrong (that is, why should it be considered to be 
	an erroneous message send)?
	
Because the expected effect of 'aSet add: aNewElement'
is to ensure that 'aSet includes: aNewElement' remains or becomes
true.  Any call for which this is NOT the case is erroneous.

A Smalltalk system should EITHER make sure that 'aSet add: aNewElement'
works for every object which could possibly be passed as aNewElement
(this does not include objects which are hidden inside the implementation
of Set and not accessible outside) OR should provide some sort of
notification whenever it is unable to acheive the expected result.

	For any method, there are only three possibilities for indicating a 
	problem:
	(1) return nil,

If C hasn't taught us the dangers of returning 0/NULL/-1 to indicate
serious failures, nothing will.  More to the point, when
'aSet add: aNewElement' *works* it returns aNewElement.
So if 'aSet add: nil' returns nil, does that mean it returned nil
because it DID add it (the usual meaning of returning the argument)
or because it DIDN'T?

So we cannot use approach (1).

	(2) return some other specialized error return value,

If aNewElement is to be usable as a Set element, it must support
these operations:
    #hash	"first thing done in Set>>scanFor:"
    ==		"used to check that a slot is NOT nil"
    =		"must work as receiver and as argument"
But *every* Object (note capital) supports those operations.
The only Object which is not allowed as a Set element,
and therefore the only Object which may not already be the result
of a SUCCESSFUL call to Set>>add:, is nil.

So there is no possible Object which could be used in approach (2).

	or (3) throw an exception.

Of the three approaches, this is the only one which can work at all
_in this particular case_.

	We also know that pattern #1 (returning nil) is used quite often
	by class designers (including the 'Set' designers of most Smalltalks,
	but not Squeak).

Calling such hacking "design" is a little extreme, isn't it?
Yes, there's at least one Smalltalk where Set>>add: aNewElement
starts out like this:
    aNewElement == nil ifTrue: [^aNewElement].
But this weirdness isn't *documented* anywhere in the sources of that
Smalltalk, not that I can find anyway.  Something as important as this
should be documented, if it's design.

	This means that it is not uncommon at all for a method
	call to return nil (such as 'aSet add:  nil' returning nil for other
	Smalltalks).

And in a certain country it is not uncommon for husbands to bring their
wives to hospital saying "the oil stove exploded" to explain the burns,
when they don't even _have_ an oil stove.  That doesn't make it _right_.

	To argue which of the three patterns is "more" appropriate has
	been a subject of debate for decades now.  My personal take on
	which is "more" appropriate is:  it just depends.  In other
	words, you can't make sweeping generalities about it, but each
	situation needs to be weighed in it's own context.

Granted that it all depends, but there is one core requirement.
You have to be able to tell the difference between a sucessful call
and an unsuccessful one.  Returning nil egregiously fails that test.

I'm only aware of one method in Squeak's Set where the public interface
depends on nil not being allowed, and that's #like:.  In effect,
    Set>>
    like: anObject
        ^(self includes: anObject) ifTrue: [anObject] ifFalse: [nil]
I don't know who SqR (who appears to have added it in 2000) is, but
perhaps s/he could comment on this rather peculiar method.  The
only use of it in Squeak 3.2 is

    Symbol class>>
    lookup: aStringOrSymbol
        ^(SymbolTable like: aStringOrSymbol)
	    ifNil: [NewSymbols like: aStringOrSymbol]

which returns a Symbol if aStringOrSymbol already has a corresponding
Symbol in SymbolTable or NewSymbols, nil otherwise.  Given that
NewSymbols is a WeakSet, it's not clear to me that it ever makes sense
for user code to call Symbol class>>lookup:.  (Reason: the result depends
on what other processes are doing and when they are allowed to do it, so
you never really know what you are going to get.)

In any case, #like: could have had a different interface:

    Set>>
    like: anObject ifAbsent: exceptionBlock
	|index|
	
	index := self scanFor: anObject.
	^index = 0 ifTrue: [exceptionBlock value] ifFalse: [array at: index]

and then lookup: would be

    Symbol class>>
    lookup: aStringOrSymbol
        ^SymbolTable like: aStringOrSymbol ifAbsent: [
	    NewSymbols like: aStringOrSymbol ifAbsent: [nil]]

Once that interface issue is fixed, there is no reason why nil should
NOT be allowed in a Set, and many reasons why it should.

	GLORP is highly reflective, and it is just passing in as the argument 
	what was returned from another message.  And, the other message used 
	pattern #1.  Consequently the set gets a nil argument.
	
If GLORP is passing nil along just like everything else,
how come it doesn't expect nil to be *treated* just like everything
else.  That is, how come GLORP expects (aSet add: x) to return x in
all cases, but expects this to add x to aSet for some but not all x?




More information about the Squeak-dev mailing list