I like proverbs.�� One of mine is "zero is also a number."
Way back in the 1980s the University in my home town was
given a new IBM mainframe by IBM.�� The Computer Centre
wrote a archival program for it that scanned each user's
minidisk (= fixed partition) for files that had not been
used for a while and logged them to a master file, then
ran a job that moved the files to a tape.
One day everything went horribly wrong.
It turned out that the reason was very simple, but so
alien that nobody had noticed when they read the
documentation.�� It was the kind of thing that was so
unbelievable you couldn't even *see* it when it was
hinted at in front of you.
Consider the following process:
�� (1) create-or-replace a file.
�� (2) write N records to it.
�� (3) close the file.
Simple.�� Obvious.�� And wrong.�� Because step (1) didn't
actually *happen* until step (3), and if N = 0, it didn't
happen then either.�� In that IBM mainframe operating system,
you literally could not create an empty file.
So when one day there weren't any files that needed
moving to tape (because they had all been done the previous
day), the process that was expected to write an empty list
of files simply left the previous day's list unchanged.
Result: thousands of multipage error reports when the
archiver tried to move files that weren't there any more.
All because someone at IBM had not been able to conceive
of a file with no records!
I hope the analogy is clear.�� But it gets better.
If you wrote a file in C on that system, it was in
effect line-buffered.�� Your text got written to the
file when (and only when) you wrote a newline
character.�� So if you imitated common MS-DOS practice
and *began* lines with a newline, the last line of your
file would never be written out.�� The equivalent of
�� s := FileStream write: 'foo text a'.
�� s nextPutAll: 'Hello world!'.
�� s close.
would write no records, and thus not create or replace
the file.�� (Because a file with N = 0 newline characters
is another of those N = 0 things that cannot exist!)
I had to redesign the I/O library for a certain programming
language that ran on a lot of Unix systems, on VAX/VMS, and
we *thought* was running well through the C library on IBM
mainframes.�� All sorts of finicky little details to deal
with N=0.�� So at the end of a file, if nothing had been
written, we wrote an empty record, unless you turned that
option off.�� At the end of each record, if nothing had been
written, we wrote a space.�� Yes, that N = 0 case was
another problem.
It has been over 30 years since I had anything to do with
IBM mainframes, but some experiences leave scars.
I note that in Pharo 9.0, there is a subtle difference
between SequenceableCollection>>copyReplaceAll:with:
and String>>copyReplaceAll:with:
and the latter is not consistent with itself.
Let x and y be Strings.
x copyReplaceAll: '' with: y
is always x, except when x is empty, in which case it is y.
Let x and y be Arrays.
x copyReplaceAll: #() with: y
is always x.�� This is not the subtle difference I mentioned
above.�� *That* difference is that the string version takes
linear time (assuming fast search) while the general version
takes quadratic time (also assuming fast search).
The ANSI specification for #copyReplaceAll:with: is quite
clear:
Answer a new collection with the elements of the receiver
in their original order, except where a subsequence in the
receiver matches targetElements.�� A subsequence in the
receiver is said to match the elements of targetElements if:
1. They have the same number of elements.
2. For all indices of the subsequence, the element in the
subsequence at a given index is equivalent to the element
in targetElements at the same index.
By this definition, an empty sequence has matches
in any sequence.
I'm not advancing this as evidence that I'm right about
#beginsWith: and #endsWith:, because the following text in
the standard makes it clear that for some reason, empty
sequences were literally UNTHINKABLE for the framers of
the ANSI Smalltalk standard, at least in the context of
search.�� I'm mentioning it as evidence that for many
people, zero is the hardest number to think of.
I've finally realised why some people stubbornly cling
to the idea that the natural numbers begin with 1, not
zero (as if the Peano axioms are not a thing).
If you are thinking of the natural numbers in terms of
*multiplication* (and primes, and so on), well,
the positive integers are a cancellative monoid with
identity 1, but not if you include 0.
If you are thinking of the natural numbers in terms of
*addition*, then the non-negative integers are a
cancellative monoid with identity 0.
So there are two *different* monoids, for one of which
including 0 would be many kinds of trouble, and for
the other of which excluding 0 would be absurd and awkward.
Strings are cancellative monoids under concatenation with
'' as the identity.�� (Don't ask me about Unicode.)�� So why
is '' so hard to think about?�� I don't think it is just
Strings.�� In functional programming, it was surprisingly
hard to teach students to remember the empty list.�� (For
myself, I got into the habit of writing the base case of
a recursive function *first* to avoid the mistake of
leaving out the base case.)�� I suspect that there are
other places with surprising behaviour for 'trivial' cases.