Dijkstra's argument is obsolete. Iterators have largely replaced the explicit specification of integer ranges in loop control.
Also, the recent rise in programming with multidimensional data invites the question of why matrix integer indices should include zero, which Dijkstra's essay doesn't address.
Likewise, the increasing use of real variables in software invites asking whether an elegant syntax for specifying integer ranges applies equally well when used for real ranges, since <= operators make less sense when constraining floats. Also not addressed.
> Iterators have largely replaced the explicit specification of integer ranges in loop control.
Yes, explicit indices are less common than they used to be due to iterator support in languages. However they are far from being obsolete. I've been recently going through a lot of code dealing with numerical calculations and there you see hardly any iterator use.
Also, MATLAB code, which uses 1-based indices, is an absolute pain to deal with compared to Python/numpy, for example.
Maybe I've been drinking to koolaide for too long (and it's not a good flavor), but the 1-based indices in MATLAB are not bad once you get used to them. They're especially nice when translating mathematical papers that normally index from 1.
I guess it depends on the field you're working with. I'm definitely more at home with 0-based languages, although at this point I have worked a lot with MATLAB/Octave code as well.
In my case, it is very common that i-th element in an array is somehow related to say (x0 + i*dx). This fits naturally with 0-based indices.
With 1-based indices you tend to have (i-1) or (x0-dx) everywhere. It gets extremely annoying when people get clever and rearrange this -1 in equations. Then it is no longer clear what is there as a side effect of the indexing and what is perhaps some constant in the numerical method.
There's a whole lot of work that goes into the high level abstractions you take for granted. The people making those abstractions care about this, even if you don't.
I wish I could somehow hide the fact that Lua uses 1-based indexing by concealing them completely with iterators.
And yet here I am, programming in Lua with 1-based indices in tables and dealing with the logical inconsistencies of interfacing with application bindings that necessarily expose C-native structures using 0-based indexing.
> the increasing use of real variables in software
This betrays the complete lack of mathematical qualification evident in the rest of your comment. IEEE numbers don't even pretend to be the reals. The most that can be said for them is that they're two copies (positive and negative) of a very small subset of the nonnegative rationals, adjoined to a couple extra symbols (+inf, -inf, NaN), with really wonky operations to work around the obvious non-closure of the rational subsets under standard mathematical operations (let alone the approximations to transcendental functions). I think it's important to be extremely clear: this is not at all like the mathematical reals, not even a tiny little bit. The use of IEEE numbers in serious applications (Bitcoin, iiuc) is truly frightening. They should be considered "useful for computer graphics" (although I can't say whether modern gpus are using IEEE numbers; they're at least useful for software rendering) and nothing more.
Hmmm, I'd say multidimensional arrays are an extra argument in favour of zero-based indexing. The position of an element in a 2d array is &(a[k0,k1])==a+k0+n0k1 (modulo syntax and ordering convention), where kx is index in direction x and nx is length of array in direction x. For a 3d array it's &(a[k0,k1,k2])==a+k0+n0(k1+n1k2). Using 1-based indexing you'd need to adjust all the kx, which would be an unnecessary waste of space and time.
Edit: Removed asterisks for multiplications, as they got interpreted as markup.
Half-open intervals are even more important when handling real ranges and float indexes. Consider splitting and joining ranges in arbitrary floating point b.
I think that the biggest reason to continue to use explicit indexing in modern programming is that this remains the most straightforward way of translating problem from the mathematical notation to code. It is not too easy for me to imagine, for instance, matrix arithmetic done using iterators. Another, even more striking, example is computational problems that are solved using methods of tensor analysis, where indices are heavily relied upon in the way things work.
For much number crunching there is often no substitute for jumping around using indexes computed by sometimes elaborate rules. Although whether to add or subtract 1 in the end is the least complicated part then, but it would have been nice if Python (0), MATLAB (1), Fortran (1), C (0), Julia (1) didn't disagree on the convention as you move between them.
Whether an array index should start at 0 or 1, and whether loop index range intervals should be open, half-open, or closed are long-standing religious debates among programmers. It seems everyone has a preference.
It's all about each language's default semantics -- what can and should be left unsaid, and how that expresses the language's model for array access, pointer de/referencing, and loop control.
For array indexing, the most accommodating syntax is to do what Fortran does: allow the programmer to explicitly specify each array index's closed array range. That may be the most user-tolerant and unambiguous model for arrays, but it doesn't work well if you also want the same syntax to dereference pointers, whose indices must start at 0. Thus array indices of pointer-based languages (always?) start at 0. But that annoys the hell out of folks who work extensively with arrays AND real numbers, where half-open 0-based integer index ranges compel clumsy semantics when iterating over their precious arrays. (Folks like me, BTW.)
> Iterators have largely replaced the explicit specification of integer ranges in loop control.
I'd agree with that for lots of domains. If you're dealing with indices often you're probably working at the wrong level of abstraction. Indices are just error prone in general even if you count from zero.
I said "for lots of domains". Obviously you have to use indices in some code but if it's possible to avoid using indices it's usually a good idea (e.g. using iterators over a standard for loop).
Dijkstra is of course brilliant and I recognize much of his tone is tongue in cheek and irreverant but sometimes I find his writing so nasty, pompous and arrogant that it becomes irritating; claiming, after a series of opinions about what he personally finds "nice" or "natural" that there is a "most sensible" way of doing things and that everyone else is being ridiculous. A tamer one of his offenses but still unpleasant.
I agree on the numbering but many people don't. He doesn't mention how nicely it works with the modulo operator, which is I think the most compelling reason. But having L[2] be the third index in an array is horribly counterintuitive and leads to countless mistakes. CS students when they begin do not like it, it is not natural. It is beaten into them by scary red error messages and instructors over months and years until they finally accept it.
I think this would help a lot for people learning computer systems as well. Matches the underlying pointer arithmetic logic. Let's all agree to switch to using offset instead of index?
As a student, I had way more difficulty adjusting to Matlab/Octave being 1-indexed than my first programming language being 0-indexed. It's very rare (and not a good sign if you're doing it: use a struct or tuple) to look for the "third" element in a list, and much more common to run through every element, and, for example, < is cleaner than <=.
It depends on the context. It's actually pretty common to make queries like "give me the third element" in data analysis, especially when your data sources come as a vaguely organized mess.
At the same time, it's not actually all that common to index elements in loops these days - most languages have some kind of a for-each construct that hides indices (and some, like Python, only have such a construct).
Yes, they are. My point was that when you enumerate collections in Python, it doesn't matter, because it's just `for x in xs: ...` - you don't actually see the indices. This is in response to "much more common to run through every element" in the parent comment - it's just not a relevant scenario for indexing in most languages today.
> But having L[2] be the third index in an array is horribly counterintuitive and leads to countless mistakes.
Really, that's just a confusion caused by language and historical accidents.
There is nothing that inherently links "one" to "first", or "two" to "second"--it just so happens that for some weird reason, it's common in English to abbreviate "first" as "1st", "second" as "2nd", and so on.
But just as "A" is the first letter of the alphabet (and not, say, the Ast), "0" is the first cardinal number, and "first" is the first ordinal number.
And there is a very simple reason why zero is the correct default first array index: Zero is the neutral element of addition, and (sub)array indexing is an additive operation.
I have to offer a counter view to this. Using letters of the alphabet as an analogy isn't accurate to me. Each letter we accept as an element... so a true analogy would be to put a Space as the first character of the alphabet, to indicate the absence of a letter.
Zero indicates the absence of a value in our numbering system. If I have a single apple, I have 1 Apple, not 0 Apples. Thus if I have 1 element in an Array I have element[1], not element[0]. It is more natural for us to use numbers not by their cardinal position in a set, but by their inferred value... because they represent quantities.
I am one of those who feels the confusion around zero bounded indexes leads to countless issues because it is counter-intuitive to how we use numbers in real life. I agree it is beaten into developers that this is how is should be, but to me we would have been further ahead to let developers accept what comes intuitively.
> I have to offer a counter view to this. Using letters of the alphabet as an analogy isn't accurate to me. Each letter we accept as an element... so a true analogy would be to put a Space as the first character of the alphabet, to indicate the absence of a letter.
So, you would say that "A" is not the first element of the alphabet or that "0" is not the first cardinal number?
> Zero indicates the absence of a value in our numbering system.
You are utterly confused. If I have an account balance of zero, that indicates an absence of a value? How is zero mathematically any different then +1000 or -1000 that justifies making it a special case?
> If I have a single apple, I have 1 Apple, not 0 Apples. Thus if I have 1 element in an Array I have element[1], not element[0].
You are mixing up ordinal and cardinal numbers.
> It is more natural for us to use numbers not by their cardinal position in a set, but by their inferred value... because they represent quantities.
There is no such thing as a "cardinal position", positions are ordinal, cardinality describes sizes.
> I am one of those who feels the confusion around zero bounded indexes leads to countless issues because it is counter-intuitive to how we use numbers in real life.
Nope, what leads to problems is that people don't understand the difference between cardinal and ordinal numbers. You cannot fix that by changing the first index, all that causes is a shift to different bugs. Including stuff like equating zero with the absence of a value.
> How is zero mathematically any different then +1000 or -1000 that justifies making it a special case?
Zero is actually always a special case in math, specially in algebra, the only number where division by it is not defined.
> Nope, what leads to problems is that people don't understand the difference between cardinal and ordinal numbers.
Actually, by Von Neumann definition of ordinals, ordinal 0 represents an empty set, thus it's equivalent of null, not single element set. So strictly mathematically speaking for non-empty sets we should use ordinals starting at 1.
But using zero-based indices is just a matter of convention, so lets not make a big philosophy out of it...
> Zero is actually always a special case in math, specially in algebra, the only number where division by it is not defined.
And what does any of this have to do with division?
> Actually, by Von Neumann definition of ordinals, ordinal 0 represents an empty set, thus it's equivalent of null, not single element set. So strictly mathematically speaking for non-empty sets we should use ordinals starting at 1.
So, strictly mathematically speaking, we should use "2 apples" to describe an apple within another apple, where both of those apples also contain nothing?
I'm sorry, but this just makes no sense whatsoever. What relevance does von Neumann's construction of the natural numbers have to this question? Especially so, given that the empty set, aka "0", is the first(!) von Neumann ordinal. We should use 1-based ordinals because von Neumann constructed 0-based ordinals, maybe?
> But using zero-based indices is just a matter of convention, so lets not make a big philosophy out of it...
No, it's not. You yourself stated that the algebraic/arithmetic behaviour of 0 is quite different from that of 1, so how do you suddenly come to the conclusion that it's merely a matter of convention?
> No, it's not. You yourself stated that the algebraic/arithmetic behaviour of 0 is quite different from that of 1, so how do you suddenly come to the conclusion that it's merely a matter of convention?
Because in programming it is, it's just a number chosen by some languages to start enumerating from because it makes things easier in some real-life cases. In pascal you could set the arbitrary start and end index and it worked just as well. It's like arguing which is a more precise way to measure temperature, in fahrenheit or celsius scale...
> What relevance does von Neumann's construction of the natural numbers have to this question? Especially so, given that the empty set, aka "0", is the first(!) von Neumann ordinal. We should use 1-based ordinals because von Neumann constructed 0-based ordinals, maybe?
You talked of ordinal numbers, so I cited one of the definitions of ordinal numbers, the one from the set theory. The similar reasoning applies for other definitions as well. Ordinal number zero is always the ordinal of an empty set. We use null for that in programming. So the next one is a single element set and it has ordinal 1. So how from this you conclude that index 0 is "the correct way" to count non-empty sets? You completely ignore the empty array that way...
> It's like arguing which is a more precise way to measure temperature, in fahrenheit or celsius scale...
Nah, bad analogy. Let me suggest a better version: It's like arguing about whether degree Fahrenheit or Kelvin is a more useful default unit to use for physics calculations. What do you think?
It's not just a number that's used to start enumerating from, it's a set of numbers used to calculate with as well, and array indexing, as I mentioned, is an additive operation, and thus using a set of indices that's not a group, for lack of an identity element, is as sensible as doing most of your physics calculations in degree Fahrenheit.
> You talked of ordinal numbers, so I cited one of the definitions of ordinal numbers, the one from the set theory. The similar reasoning applies for other definitions as well. Ordinal number zero is always the ordinal of an empty set.
Exactly. And you realize that that is the first ordinal number, right? "First" means "the one without a predecessor", and the empty set, also represented as a zero in that context, is the element without a predecessor, and as such the first ordinal number of that construction. Now, what is your point?
> We use null for that in programming.
We use null to represent the first ordinal number? What kind of weird languages are you using?!?
> So the next one is a single element set and it has ordinal 1. So how from this you conclude that index 0 is "the correct way" to count non-empty sets? You completely ignore the empty array that way...
First of all, it seems like you are utterly confused about the relevance of von Neumann's constructions of ordinal numbers to how to index arrays. There is none. It's simply an exercise in finding an isomorphism between sets and "normal" numbers.
Also, no, I don't ignore the empty array. An empty array has no positions, therefore, there is no ordinal number that could refer to a position in an empty array. If you have an index pointing into an empty array, you are doing something wrong. You cannot point to the fifth element in a four-element array, you cannot point to the second element in a one-element array, and you cannot even point to the first element in a zero-element array.
If you want to be able to have a data structure that can represent an index into an array, or alternatively the absence of an index (for whatever reason--one reason might be because the array is empty), that's your problem, not the problem of the index type. Why should we reserve perfectly useful values of data types to represent stuff that's logically separate from the type? How about we reserve the value "2" in floating point values for cases where you want to store the word "honey" as a floating point value? That's just crazy. You can possibly use such sentinel values in your software for performance reasons or whatever, but limiting sensible use by the general public because you want to mix concerns?!?
The third element of the sub-array starting at the second element of an array is which element of the base array?
With zero-based indices, that is a perfectly normal additive operation: 1 + 2 = 3 --the element is to be found at index 3.
With one-based indices, you resurrect all the crazyness of algebra and arithmetics before the zero was invented, just because you want to use the zero for something else: 2 + 3 = 5 ... and the index is 4, obviously!
I agree with you and also with the person you replied to. But more generally -- is it fair to say that cardinality describes size as opposed to quantity? If you go with the quantity concept instead then 1-based numbering makes sense and agrees with ordinary usage.
(Disclaimer: I used to hate 1-based indexing, for reasons similar to what you are saying, but I'm brainwashed by R these days and now rather like it.)
> is it fair to say that cardinality describes size as opposed to quantity?
Nope, size is equivalent to quantity in this context. But ordinals describe position in an order as opposed to size/quantity. If you want to connect them mathematically, you would usually map a scale to a quantity measure, where the first unit position on the scale is the zero-position, the second unit position is the one-position, and so on--so, a quantity of size three units reaches from the position 0 on the scale to the position 3 on the scale.
> (Disclaimer: I used to hate 1-based indexing, for reasons similar to what you are saying, but I'm brainwashed by R these days and now rather like it.)
Well, it doesn't matter unless you have to to arithmetic with the indices, you might as well label them with letters from the alphabet, so if you don't need that, it's simply a matter of getting used to it. But in general-purpose programming languages, array indices are often computed, and 1-based indexing is as sensible there are having an integer type that doesn't have a zero. It's not that you cannot work with it, but it makes things unnecessarily complicated.
I am one of those who feels the confusion around zero bounded indexes leads to countless issues because it is counter-intuitive to how we use numbers in real life.
What's counter-intuitive to me is that we habitually call the second floor in a building the "first floor"!
Of course there is something that links "third" to "three", "fourth" to "four", etc. It's called language. So it's not surprising that we abbreviate "third" with "3rd".
The historical "accident" is that we don't start with "zeroth". That's probably what you wanted to say but the reasoning tripped me up.
Well, yeah, I didn't want to go that deep into it, of course there is a reason why they are linked. But none that links them inherently, which is what I wrote. And the accident really simply is that "zero" wasn't an accepted concept yet when ordinal numbers were invented, which led to the first ordinal number being called "first" and not "zeroth". Meanwhile, humanity largely has accepted zero as a useful and coherent concept, but just didn't bother to update the names of ordinals to fit better with that, which is where the confusion comes from.
So many words, of vague meaning (e.g. "inherently"), and confusing levels of quoting (e.g. first vs "first"). What's wrong with just saying "Unfortunately we don't start with 'zeroth'"? Can't discover more meaning in these paragraphs.
I think this hits the nail on the head. Counting things from 0 instead of 1 would be nice in the real world too, not just in programming. For example, in a base 10 system, you could have exactly 10 pigs with single-digit numbers painted on their sides. That makes much more sense than the current system, where the last pig requires a two-digit number for some reason. Tonight I'll go to bed happy in the knowledge that programming isn't wrong, it's the world that is buggy.
To expand on my comment below, I assumed zero-based indexing was a natural extension from binary. When using bits (in this case three bits) as a representation, you start with 000. Naturally that would be referred to as 0.
Well, yes, but: That ultimately is also an arbitrary definition, in a way. You could also build a computer where 000 would represent the integer 1. And the reason why that is not the case, but 000 instead represents the integer 0 is essentially what I said above: It's simpler to build a binary computer that computes 000 + 000 = 000 and 000 + 001 = 001 and 001 + 001 = 010, instead of one that computes 000 + 000 = 001 and 000 + 001 = 010.
In this sense everything is an arbitrary definition—which is true, but makes arguments about naming sort of useless. (Perhaps the correct answer is that they are, and this is just revealing that ….) At a certain point the argument, if it is to be at all productive, must become not "what is the one and only true way to do this?"—there never is one for naming—but "what are some other conventions that might guide us to a well determined consistent, rather than arbitrary, convention in this case?"
> In this sense everything is an arbitrary definition
Hmm, no, not really. While definitions are, indeed, by definition arbitrary, different concepts are of different usefulness, and as such, definitions that use more useful concepts can reasonably be considered better definitions of given terms.
There is no such advantage in using the binary 000 to represent the first index, though, except in so far as it results from the binary 000 representing the neutral element of addition, which corresponds to the natural number 0. The latter is the part that's actually not arbitrary.
> there never is one for naming
Well, the whole point kindof is that it's not about naming, but about arithmetic. If you only want to name stuff, you can use strings and hash tables, or letters of the alphabet, or whatever other set of symbols that you can come up with, it's really to a large degree a matter of taste. But there is a distinctive feature of arrays vs. other collection data types: You can do arithmetic on its indices. And arithmetic with a number type that doesn't have a zero is braindead.
Linking ordinals to cardinals is done by the same abstraction process that first led us to think of numbers as distinct objects. We noticed there was a property in common between the set of our five fingers and a pile of five sticks. And hey, it's not so different from the property of the fifth object in some sequence, so let's lump those together. They're formally different, but intuitively similar.
...but isn't the most salient point of the fifth object that there are 4 objects preceding it? (there could be any number of object after it, and it would remain the fifth).
To play devil's advocate -- and let's be clear, confusing ordinals and cardinals really is the devil's work ;) -- there are 5 items not after the fifth item.
The ridiculousness of that argument should be evidence enough that the fifth item is item 4.
In my teaching I try to distinguish between conventional-language numbering by saying "first", "second", "third"; from zero-based indexing by saying "zero-th", "one-th", "two-th", and so on.
It's a bit awkward, but once explained it is a quick way to tell the two numbering schemes apart.
Hmm, I dunno, I guess it might be a useful tool to explain the distinction? Not sure whether, in order to avoid long-term confusion, it would still be good to explain that, really, "first", "second", and so on, are the common words other people use for what you call "zero-th", "one-th", and so on?
I understand the mindset that creates this, but I have a different one, I think?
Consider the physical world. Let's say you've got three fenceposts (of unit width) in a line standing before you, each separated by 0.5 fencepost-widths of space.
It makes sense to say "the first fencepost." That's the post can be found in the (0.0, 1.0) region of the fence.
But "fencepost 0" or "fencepost 1" are both ambiguous to me: neither 0 nor 1 is inside the region the first fencepost occupies, physically. The mapping isn't intuitive. Both of those numbers point to where the fencepost isn't: just past its ends. Depending on your mindset, you might want to refer to the fencepost either to the left or the right in the row of that imaginary separating line: either "the fencepost that was just completed" or "the fencepost that is about to start" in your visual stream. Even worse if you're from a culture that reads right-to-left.
In physical terms, if you wanted to give "the first fencepost" an address... it'd be fencepost 0.5! Likewise, "the second fencepost" would be fencepost 2.0, and "the third fencepost" would be fencepost 3.5. These numbers refer to the centers of each fencepost. You don't have to ask the question of which "direction you head" to find the fencepost from the address: when you're at the address, you "have" a fencepost. You just need to know the origin and axes of the fence and you can "go to" the fencepost.
Now, we could get fencepost addresses of 0.0, 1.0, 2.0, and so on, by following some indexing strategy like "the position of the left edge of the Nth fencepost, measuring only fencepost-widths and not space between fenceposts." This kind of offset-based indexing is "intuitive" when talking about memory addresses, because memory addresses are like an ideal fence: every post is 1.0 units wide, with no space in-between posts. (Also, you can't actually point to the centre of an atomic unit of memory; memory addresses are quantized such that the only place the "handle" of a memory address can be attached to the memory itself is the "left edge"—the beginning—of the memory.) Great for digital logic, but we just don't have anything like that in physical space.
In reality, when we want indexes, we explicitly label things with numbers from a monotonic sequence, and then either write down mappings from the center-of-measured-interval addresses of the physical world (e.g. GPS coordinates) to those labels, or, worse, binary-search our items for one with the right address-label, hoping the labels have stayed monotonically sorted since they were issued.
There are nearly no cases where there is a natural "element zero" that exists at zero in the physical world, to create that associative intuition (perhaps, people crossing a finish line in a race: the winner is certainly "person zero" in the photo.)
> But "fencepost 0" or "fencepost 1" are both ambiguous to me
Which is good, because it means that you are not confused :-)
The confusion is the idea that there is an inherent link between "1" and "first" or "0" and "first", which there isn't, because one of them is a label, while the other is an ordinal number.
You need a definition of how to map one to the other, and as long as you don't have that, the labels do indeed remain ambiguous, which you correctly recognize.
Now, there is an argument to be made for why array indexing should use a definition that maps the first element to label 0, and that is, as I wrote, that array indexing is an additive operation.
As for how that connects to your fence example: What you necessarily need in addition if you want to model your fence with an array(!) is a definition that maps your fence posts' positions to ordinal numbers, which you then can map to array indices via the definition above. That exact mapping is necessarily arbitrary to a degree, and the only really important thing is that you are consistent.
But within the implementation, you are likely to encounter problems such as "find the third fencepost of the part of the fence that starts at the second fencepost". And that would be the fourth fencepost of the overall fence, not the fifth, or in indices: 1 + 2 = 3 vs. 2 + 3 != 4.
> There are nearly no cases where there is a natural "element zero" that exists at zero in the physical world
Well, nor is there a natural "element one" that exists "at one".
> Well, nor is there a natural "element one" that exists "at one".
Right. I'm on the "no natural mapping" side as well, here, not with the "first = 1" people.
Actually, consider for a moment, a programming language that tried to avoid both of these mappings, at least in its programmer-facing syntax (it might compile down to one or the other mapping, but that's an implementation detail.) It might not be so bad!
Of course, you wouldn't be able to index arrays by a single ordinal number at all, instead being forced to do speak in terms of intervals:
Effectively, the single-element index operation would just be a specialization of a slice operation. Both operations would take a mantissa indicating where the "left edge" and "right edge" containing the value you want is in the array's memory space. And you'd be forced to specify explicit units for the "addresses" of the mantissa, even though the language might reject a mantissa of the wrong type.
This seems kind of annoying to write. But, I swear, it feels so pleasant to read. Much moreso than any language I've used. It's not hard at all to "keep the logic in my head" when I read the above back.
> This seems kind of annoying to write. But, I swear, it feels so pleasant to read. Much moreso than any language I've used. It's not hard at all to "keep the logic in my head" when I read the above back.
Well, yeah, but you conveniently left out index arithmetic :-)
If it's only about hard-coded indices, it really doesn't matter, IMO, as long as it's consistent, and you could just use any ordered set of symbols for indexing, and let the compiler figure out the mapping to binary numbers and machine code that's functionally equivalent.
The interesting case is when you, as the programmer, want to do arithmetic with indices. Of course, you could have a separate number type for array indices, say. But there really is no way around making its algebraic behaviour isomorphic to 0-based indices, because 1-based arithmetic is hell, and if you do that, it's probably a good idea to use the established names such as "0" or "1" for the established concepts such as "neutral element of addition" or "neutral element of multiplication", in order to avoid confusion.
Without. The dyadic arr[range] operator would always return one (unwrapped) element, a bit like how SQL ORMs frequently have a Query#get_one method to match Query#get_all. `letters` was a char[], `pairs` was a short[], so you're grabbing a short by its interval-address (+0 shorts from origin, +1 shorts from origin).
(Or, to clarify if you were confused by the not-quite-C pseudocode, by 'AB' I meant the short 0x4142.)
> But there really is no way around making its algebraic behaviour isomorphic to 0-based indices
Oh, sure, it's "zero-based arithmetic" because we're still fundamentally dealing with offsets (and when you slice the array, you take offsets from the beginning of the slice, etc.)
I think it's just better—in some vague human-factors sense—to be forced to mention, in each indexing operation, both where the offset (position of hyperplane separating the element from its previous) and the offset+size (position of hyperplane separating the element from its successor) will land, so that you realize explicitly what two numbers those are in your program, as you write them.
Humans—especially beginner programmers—don't intuitively realize that array indexes are offsets. They just want to talk about cardinal-numbered positions, and find themselves unable to, so they guess at a mapping and make mistakes.†
This syntax sort of short-circuits that by giving them an indexing operation that doesn't ask them to decide on any cardinal:ordinal mapping at all. It just asks for two numbers that are clearly "offsets of the positions between array elements", rather than offsets (or counts) of elements.
But even for experienced programmers who understand that indexing operations are pointer offset math, such an indexing-by-continuous-slicing scheme is potentially still a bit helpful, somewhat like being asked to write out a deductive proof for a hand-wavy argument: it makes you aware of when you've made a leap of logic that has no rule allowing you to make it. Or, in this case, it makes you aware of what element you're actually capturing with your hand-wavy math.
> The interesting case is when you, as the programmer, want to do arithmetic with indices.
I think—and this I find even weirder to say than my first thought—that it might be good to restrict this "humane" indexing operation from containing compound expressions.
Instead of this:
foo[(a * 3 + 2)ch .. (a * 3 + 5)ch]
...you'd be forced to write this:
offset = a * 3 + 2
foo[(offset)ch .. (offset+1)ch]
or maybe the x86 LEA-alike:
pos = a * 3 + 2
foo[(pos)ch : 0ch..1ch]
This forces the programmer to give a name to the number they're using as the "base" in the indexing operation—which pushes them even further toward noticing when they've borked up the fencepost math.
---
† A tangent: "pointers are hard" for beginning programmers because C tries very hard to make array access, and even plain pointer math, "act like" reference-by-cardinal-rank, but then still uses zero-based offsets, munging up the ability to deduce a coherent semantics of pointers as memory-address-valued variables.
In C, when you add an (ordinal!) integer to a pointer, it multiplies that integer by the sizeof() the pointer's referent type because it wants you to model the pointer as being an iterator-handle for a collection of things, rather than just being a memory-address-valued variable. It makes sense, for practical reasons, that you can't create a short︎*-typed variable containing a non-short-stride-aligned memory-address... but that should be a domain error on the result of the addition, not something you're mathematically unable to express.
> But even for experienced programmers who understand that indexing operations are pointer offset math, such an indexing-by-continuous-slicing scheme is potentially still a bit helpful, somewhat like being asked to write out a deductive proof for a hand-wavy argument: it makes you aware of when you've made a leap of logic that has no rule allowing you to make it. Or, in this case, it makes you aware of what element you're actually capturing with your hand-wavy math.
Well, I'm not convinced, but who knows? My guess would be that it wouldn't really reduce mistakes much. People would simply memorize the rule that for a single element, you always have to give some index and then follow that by index + 1 --and if the index is computed incorrectly, the result is just the same wrong result as before. You cannot force people to prove that the offset is correct by forcing them to write down the expressions for two values that have to always differ by exactly 1.
> In C, when you add an (ordinal!) integer to a pointer, it multiplies that integer by the sizeof() the pointer's referent type because it wants you to model the pointer as being an iterator-handle for a collection of things, rather than just being a memory-address-valued variable. It makes sense, for practical reasons
Nah, I think that's perfectly sensible, even theoretically. A pointer is a perfectly sensible memory-address-valued variable, it's just that it's not in "machine code addresses", because that is an implementation detail, but rather in "abstract address space addresses", with a separate address space per object (by which I mean an allocated memory region), and obviously, with arithmetic in that abstract space counting in elements of that respective address space's type. Why would counting in bytes make any more sense? The size of a byte (or a char, rather) is mostly an arbitrary implementation detail that abstract code shouldn't be bothered with.
BTW, what you write is kindof wrong: As per the abstract definition of the language, nothing is being multiplied by sizeof()s. That tends to be how it's often implemented on the usual platforms, but the abstract definition simply says that arithmetic with pointers works in units of elements, and that consistently: If you subtract pointers, you get the number of elements between them, not the number of bytes/chars/octets/words/whatever between them, the internal value is not really visible anywhere, and could relate to the abstract value in arbitrarily complicated ways anyway (like, you could have pointers that are implemented with tags to differentiate pointers to different machine address spaces and stuff like that).
> that you can't create a short︎*-typed variable containing a non-short-stride-aligned memory-address...
That's actually not the goal nor the case. Whether a pointer is aligned is an implementation detail, there is nothing in C that would require short objects to be aligned to short boundaries, and what implementations do simply depends on (a) whether the platform can handle non-aligned pointers, and (b) if it can, what makes sense as a matter of weighing performance vs. memory usage. I haven't checked it, but my guess would be that the AVR GCC, for example, does not align anything, because it only needs more memory and makes no difference whatsoever for performance (I think ... maybe I am forgetting something).
This sounds like sophistry. There are no 0 things in the world, it's just an abstract concept. I have 1 car, not 0. If I had a lineup of cars, the first one would be 1, not 0.
> There are no 0 things in the world, it's just an abstract concept.
So, empty water bottles don't exist? Or do you object to saying "this bottle contains 0 ml of water"? Why? Or do you think that no water bottle is the same thing as a water bottle with no water in it?
> I have 1 car, not 0.
And what about someone who has no car? Are there no people who don't have cars? Or do you object to saying "this person has zero cars"? Why? Or do you think that no garage is the same as a garage with no car in it?
> If I had a lineup of cars, the first one would be 1, not 0.
No, it wouldn't. Unless you arbitrarily decided to label it as such. It would be the first. That's an ordinal number. Also, you would have 1 car. That's a cardinal number.
>> So, empty water bottles don't exist? Or do you object to saying "this bottle contains 0 ml of water"? Why?
They exist conceptually, because a water bottle could be empty of anything that fits inside. We say it has 0 ml of water and not coke or sand because we've designated it to be a water bottle, but nature obviously doesn't care what goes in the bottle, as long as it fits. It's just a useful idea we have to say there is zero something to designate a lack of something. But there isn't zero water in nature, anymore than there are zero unicorns, or zero Kings of France.
>> And what about someone who has no car? Are there no people who don't have cars? Or do you object to saying "this person has zero cars"? Why? Or do you think that no garage is the same as a garage with no car in it? <<
Are there no people who don't have dinosaurs or time machines? The things we have zero of are infinite. But you don't actually have zero pink elephants in your backyard.
>> No, it wouldn't. Unless you arbitrarily decided to label it as such. It would be the first. That's an ordinal number. Also, you would have 1 car. That's a cardinal number.
So if we were to label to cars in increasing numeric order from one end to the other, we would start with 0 instead of 1 at the first car? We don't do that with building levels. You enter on the first floor, go to the elevator, and you push a button for the floor level. The first floor is 1. 0 Would be the basement level.
I feel zAy0LfpBZLC8mAc responded to the rest of your post fine - so I'm only going to add my own opinion regarding your last statement:
>So if we were to label to cars in increasing numeric order from one end to the other, we would start with 0 instead of 1 at the first car? We don't do that with building levels. You enter on the first floor, go to the elevator, and you push a button for the floor level. The first floor is 1. 0 Would be the basement level.
We opted for the word "Ground Floor" instead of "Zeroth Floor" likely due to the associations people make of "zero" (ie. not existing) and because it may have sounded weird but the concept is the same.
The U.K and U.S refer to different storeys as the "first floor". In the U.S, the first floor is considered the ground floor and in the U.K the first floor is considered the second storey. A source of headaches when told the place you're looking at is on the first floor and you spend ages looking for it on the ground floor.
> It's just a useful idea we have to say there is zero something to designate a lack of something. But there isn't zero water in nature, anymore than there are zero unicorns, or zero Kings of France.
That's just confusion on your part. All of language is just a useful idea we have to designate whatever we designate with it, and you are just confusing things for no reason at all. You obviously understand what "zero something" means, and that what it means is indeed something that can describe aspects of reality, and then you go on and claim that somehow it doesn't. When I say "there are 0 ml of water in this bottle", you understand what it means, right? And if there is indeed no water in the bottle, what is there to be confused about? It does mean that there is no water in the bottle, but it also means something else at the same time? Or there is no water in the bottle, but there is also not no water in the bottle?
> The things we have zero of are infinite. But you don't actually have zero pink elephants in your backyard.
Yes, I actually have. "having zero <whatevers>" is, as you evidently know, a synonym for "having no <whatevers>" or "not having <whatevers>", and I absolutely certainly actually do not have pink elephants in my backyard.
> So if we were to label to cars in increasing numeric order from one end to the other, we would start with 0 instead of 1 at the first car?
There is no such thing as "we would start". You can start wherever you want. That is, as derefr has already explained to you, an exercise in assigning serial numbers. You can use whatever numbering scheme you like. Obviously. Some might be more useful than others, though.
> We don't do that with building levels. You enter on the first floor, go to the elevator, and you push a button for the floor level. The first floor is 1. 0 Would be the basement level.
Except you don't enter on the first floor. The "first" thing in any order is defined to be the thing without a predecessor. Obviously, if there is a basement, the ground floor is not the first floor, because the basement precedes it. Just because the ground floor is commonly called the "first floor" for historical reasons, doesn't make it the mathematically first floor.
> We don't do that with building levels. You enter on the first floor, go to the elevator, and you push a button for the floor level. The first floor is 1.
In some places (e.g., most of North America), that's the convention. In others, the "first floor" is the one immediately above the ground floor.
You're getting confused between cardinal and ordinal numbers.
Ordinal numbers (0, 1, 2) map to the world as amounts, and certainly it seems 'real' that there can be "one of something" in a way that it isn't that there is "zero of something" (though it isn't, really.)
Cardinal numbers (first, second, third) map to the world as rankings or positions. Positions, further, often almost always map to distances from an origin. "First" really almost always means "the item that is +0 positions away from the origin." The person who wins a competition ("first place") is zero positions down the leaderboard. The "first" book on your book shelf can be found +0cm away from the left edge of your bookshelf. Etc.
With your cars, you're talking about a third thing, though: serial numbers. "Car 1" is what you might call the first car you bought (if you were planning on buying several), because we conventionally assign serial numbers to things starting with 1. But that has nothing to do with counting or ranking; it's just a convention (as can be seen when we choose to use an alternate scheme, like UUIDs, for labelling items in a database.)
What we're talking about here is the relationship between counting and ranking: between cardinal and ordinal numbers. Since they're both abstract, and not structurally connected in any important way, there's actually no "essential, required" way to relate them to one-another, and we have to just make one up. Some people relate "first" to the natural number 0; some people relate "first" to the natural number 1.
His paper "How Do We Tell Truths That Might Hurt?" Is a good example, IMHO:
It is practically impossible to teach good programming to
students that have had a prior exposure to BASIC: as
potential programmers they are mentally mutilated beyond
hope of regeneration.
The use of COBOL cripples the mind; its teaching should,
therefore, be regarded as a criminal offence.
APL is a mistake, carried through to perfection. It is the
language of the future for the programming techniques of
the past: it creates a new generation of coding bums.
The use of anthropomorphic terminology when dealing with
computing systems is a symptom of professional immaturity.
More:
I think of the company advertising "Thought Processors" or
the college pretending that learning BASIC suffices or at
least helps, whereas the teaching of BASIC should be rated
as a criminal offence: it mutilates the mind beyond
recovery.
Given, I'm cherry-picking here. Not everything Edsger says is like this. Not by a long shot. He is brilliant, make no mistake.
But I must leave you with a quote from Sir Alan Kay that I found on the Wikiquote page while I was researching this:
You probably know that arrogance, in computer science, is measured in nanodijkstras.
Given, it's Alan, so I don't know how serious he was being.
I think finding fault with this kind of opinion is a symptom of the modern concept of "taking offence". Ie nobody can express an opinion without also showing that they respect others' opinions. But that's not right. We don't have to respect each others' opinions if we think they are wrong. What we must do in a free society is respect their right to have, and express, a different opinion.
Dijkstra clearly thinks that teaching BASIC is damaging people's brains so he says that loudly and clearly with a touch of humour. Should he water it down to avoid offending people that like BASIC?
When he has no argument other than the insult no, it really isn't funny. Why is understanding Cobol and BASIC a crime? No idea, only that the great and powerful EWD says so.
How could you possibly take that away from the quoted material?
Firstly, he doesn't say that understanding COBOL and BASIC is a crime: he says that teaching COBOL and BASIC should be regarded as a crime. And he lays out why: he says that exposure to those languages causes some kind of mental damage, so he is likening the teaching of them to the intentional infliction of an injury. Clearly this is tongue-in-cheek - it is highly unreasonable to infer that he literally wanted the teaching of two programming languages added to the criminal code, because this is absurd on its face.
The intended takeaway is that there are habits of thought learned by use of BASIC or COBOL that are must be unlearned to think about programming problems in that way that Dijkstra recommends.
Dijkstra has every write to voice his opionion however he pleases. And this isn't offending people who like BASIC. BASIC is a stupid language.
What it is about is how arrogant and absurd it is to say that learning BASIC causes brain damage. If people couldn't re-learn and develop new ideas, we wouldn't even have computers.
> But having L[2] be the third index in an array is horribly counterintuitive and leads to countless mistakes.
Keep calling it the second, then. And use 'zeroth' for L[0]. Problem solved.
Also, what is 'counterintuitive' is very often just a question of habit. I've many times seen Americans (at least those not scientifically inclined) express confusion over metric units. I have been using the metric system all my life in every its domain: I buy apples in kilograms, measure rooms in metres, water in litres, and I regard it as the most natural thing in the world. I find the American unit system batshit insane (why is 'ounce' a unit of mass, but 'fluid ounce' a unit of volume? WTF is up with all those different conversion factors?) and if I had the power to wipe the it from the face of the Earth, I'd do it in a heartbleed. I'm sure that if I asked some people from my country about this, I'd find many agreeing with me. But I realise there are people who feel just the opposite. (I still think they're wrong, but I get where they're coming from.)
Zero-based indexing is similarly just a question of what people are used to. When those students start appreciating the nicer mathematical properties of zero-based indexing (and half-open ranges that go along with it), it'll become second nature to them. (I suspect the reason why many people don't is because laymen don't often work with abstract algorithms and indexed data structures where those properties display their usefulness.)
I'm glad you wrote this. As a non-programmer but someone who was a math education minor 20 years ago, I was really having a hard time with some of the statements like "That is ugly" or "starting with 0, however, gives the nicer range." I was trying to think of a reason why it would be computationally inelegant or something.
L[2] has nothing to do with "second". It just means "plus 2".
If you consider Monday the first day of the week, then Monday[1] is (naturally) Monday + 1.
Zero indexing just feels right, I can't help it (it's written in our DNA).
I started with arrays with Fortran. There can pass an array as an argument to a parameter of a subroutine, and the subroutine can address the darned array
any way it wants. Why? Sure, maybe want to sort the array based on a slice here omitting details. Sure, such programming would need a lot of well done documentation and still is dangerous. And such array fooling around stands to get into trouble with compiler options to check array bounds.
Then I did a lot with PL/I: There get arrays, structures, structures of arrays of structures of arrays, etc. Also get to set the lower and upper bounds of each array index to anything -- positive, negative, or zero, including during execution -- that can be represented in the usually 16 bit array indexing arithmetic. And, it turns out, all that array indexing, even for arrays of structures of arrays, etc., is just ordinary multi-dimensional array arithmetic -- nearly as useful as objects but with much simpler storage allocation and array indexing. And if look carefully at that arithmetic, then do see how to have any subscript (index) lower bounds and upper bounds with just trivial modification of the usual array indexing.
And, yes, when I wrote C code, no way did I like the high emphasis on starting an array at index 0. Besides C doesn't even really support arrays, e.g., with several subscripts, and forces a programmer to write a macro or some such that does the arithmetic. And there are problems in a subroutine that receives such an array. And compiler support for checking array bounds is in trouble because the language syntax and semantics don't even support arrays with several dimensions. So, have to write, say, a matrix package that has a C structure for the details of number of dimensions and the upper/lower bounds for each dimension -- it works but get no compiler based debugging support or I/O support, and no doubt the compiler can't do some indexing optimizations that Fortran and PL/I can do because the data being used for the indexing is, in the semantics, not subject to being changed by the user's code during the indexing arithmetic. E.g., with PL/I, in the execution logic, each structure or array, each aggregate has a little table, a dope vector that describes the aggregate, yes, within a subroutine and within runtime subroutines, e.g., for doing I/O on the whole aggregate.
PL/I was in good shape before C was developed. PL/I was used for the operating systems Multics and much of Primos. So, PL/I offers programmers much more than C and, still, is good for both applications and operating system code, especially in array indexing.
I didn't see that in K&R, 1988. Are such multi-dimensional arrays new since 1988?
When I needed to do a little matrix work in C, I just wrote my own little package of matrix code, maybe with more functionality than I just saw in your first link. Maybe I didn't really need to write my own?
I'm not sure. I only started learning C around 1991. I could see how you might still have to work around using the arrays. "a[4][4]" as an example allocates memory contiguously, but if you pass a pointer to a, you lose the dimensionality (it's more like "a[16]"). So you'd have to have some other way to pass the dimensions in and you'd have to calculate the location of elements yourself. Otherwise, you'd have to pass it by value (if the dimensions are known at compile time and don't change) which can be very inefficient (copying large blocks of memory). I thought maybe you were getting at that when you said C doesn't "really" support it. The more I look into how to pass a multi-dimensional array around in C, the more I think you are more right than I was. It doesn't really have it, it's more like a "recursive use of the type system" as one link was explaining it.
I know he is wrong as I have a proof by contradiction, 0-index felt natural to me. So I do not have to speak for anyone but myself to highlight the overreaching nature of the claim.
Frankly, I find Dijkstra's writing horribly grating and irritating. The majority of his articles seem to be just him insulting things he doesn't like, another thing I find grating (for one thing, I can't think of anything he does like off the top of my head). He also seems to have a habit of passing his opinions off as fact.
But he is really smart, and often right, so while I can be annoyed by him, I can't actually ignore what he says, because it does matter.
I always that the best argument, that every assembly programmer knows, that starting at i=0 allow i to be used as an offset from the base address of an array. I bet that's why K&R choose this for C (unlike Wirth for Pascal which allowed any index range for arrays).
This makes the most sense to me if you look at it backwards. Given that array access starts with zero, what do you call the access parameter? If you call it "offset" from the beginning of the array, then starting at zero makes sense. If you call it "index", or "element", then we have this confusion. The problem to me isn't the choice of the numbering scheme, it is the names and explanation around it.
Which makes sense that it's offset for C, but higher level languages like Python, JS, etc should be index based, which would be 1. That's where R, Julia, Matlab get it right.
Actually Niklaus Wirth did the best thing a long time ago with Pascal ... let the programmer decide the index range for arrays (although he screwed up making the size of the array part of the type). K&R made the best decision for C since it is the most transparent for the hardware.
<sarcasm>
We should also teach children to start counting at 0 instead of 1. While we're at it, tell them that we only have 8 fingers (not 10), and thus octal would be the conventional base and we can use our thumbs for carry and overflow bits.
</sarcasm>
Yes, it is possible to convert from any numeric base to any other numeric base.
Why is 2 so special? Is it more important for humans to be able to calculate quantities in their head, or to be able to convert to obscure computer formats?
Of course you can convert between any two bases. It's trivial to convert between bases that are a power of two. I don't know of a trivial way to convert between base 12 and base 2. 2 is very special to computers -- that's the base they use.
> We should also teach children to start counting at 0 instead of 1
We do, not when they're starting out but the number line is introduced pretty early to children. Even earlier we teach them to count backwards with zero being the final element. Kids aren't strangers to the concept of zero.
Indeed, and this is also consistent with C being closer to the metal than Pascal. At a hardware level, a typical implementation for array access was to have a base pointer plus an index, where the base pointer points to the start of the array, and the index is the element number. I got used to zero based arrays thanks to assembly language, long before learning C.
I don't know if this is still the case -- my understanding of microprocessor architecture is about 30 years old.
Dijkstra's handwriting is quite charming. If you've never seen it, it's worth clicking the “EWD831” link at the top right to see the scan of the handwritten original.
Last month I went to my parents' house and cleaned out a bunch of my old crud. Which included school notebooks and books back to 1980 (at which time I had access to computers and typed all my papers). I was shocked (as was my kid) at how clear and legible my old notebooks were.
Nowadays I do write stuff but summarize it into the computer. I've tried various tools for automatic capture (e.g. livescribe) but they don't work that well. I realized that the probably would have worked great with my 35-years-ago handwriting.
I do wonder whether given enough effort you could train an OCR program to be pretty good at recognising one person's handwriting. Like if you meticulously corrected the output and added those corrections to your training set, would it eventually be perfect (or close enough anyway) or would it hit some sort of fundamental limit?
Clearly some peoples' handwriting sure. Good handwriting recognizers take advantage of stroke recognition, which you can't do by OCRing after the fact.
In addition my handwriting has become terrible. If I check my notes soon after writing them (within the same day) I can often tell by making out a few words and remembering what was being discussed when I was marking that part of the page :-(. A couple of days later I can barely understand them and longer than that I might as well forget it.
I could probably get by making random marks on the paper!
It's so sad, because when I was a scientist and had to maintain a lab notebook other people could easily read my notes (and they had to be clear for IP reasons). Or maybe I should look at it the other way: it's not sad, because it doesn't matter as much any more.
Yes. In fact, the way he used hyphenation at the end of a line made me go back and look at each letter to make sure it wasn't a font. They are very consistent, but not exact clones. Makes me wonder if there are ways to make fonts that use multiple versions of a letter to make them appear to be more hand written. I bet you could program one of these to do it: https://www.youtube.com/watch?v=ce35RAMMQug
The OpenType [1] font format does allow for stylistic alternates [2], which is basically what you're suggesting, in that different forms of characters are included and can be selected based on rules (like ligatures) or I believe selected at random. When used with a handwriting font it does more closely resemble natural handwriting to the casual observer.
I hate to disagree with Dijstra, but it looks like Comic Sans. It's not joined up; I think it's typical of draftsman's annotations on technical drawings rather than handwriting.
(Charles Rennie Mackintosh has particularly lovely drafting letters; turned into the Hill House font.)
Edit: I can't explain how, but the first time I downloaded that PDF I received a file which looked different. Here is a similar file from the same site which clearly resembles what led me to make the comment above:
He writes his name as something like Dÿkstra, but everywhere else it's referred to as Dijkstra. Does anyone know how this character (combination) works?
It is interesting to see the same question applied to natural numbers, and Dijkstra landing on the same [x, y) recommendation. I'd say for natural numbers the argument is even stronger, since there is a least possible value.
0-based arrays are IMHO simply a consequence of C's array access operator given transparent semantics in terms of pointer arithmetic.
Try some string manipulation using the awk language, which has 1-based strings and arrays, and you'll see that programs and string expressions are generally shorter and more idiomatic.
Later languages building on awk such as Java and JavaScript (awk is almost a subset of, and the inspiration for early JavaScript) have cargo-culted 0 into the language as base offset. Though probably it may have prevented some errors to make commonly used languages behave same in this respect.
> ... 1-based strings and arrays, and you'll see that programs and string expressions are generally shorter and more idiomatic.
Depends very much on what you're doing. If you're doing, say, a filesystem driver, and you need to do math or reasoning on [offset, length] pairs quite frequently, you will find 0-based indexing much more natural. At least that's been my experience. I think some of this applies to strings and memory too.
The way I tried to resolve 0 vs 1 based indexing in my head was is they have slightly different contexts. 0-based indexing tells you have far you are from the origin, the first element whereas 1-based indexing is useful for when thinking about how many elements you have.
Ah, counting vs measuring! That makes a lot of sense.
In the west, we measure our age and start from 0. In China, they count their age (number of distinct years someone has been alive in) and thus start from 1. In fact, in China, if you are born on new years eve you can be 2 years old at 2 days old!
If you represent base-b coefficients as such:
a = a[0] b^0 + a[1] b^1 + b[2] 2^2 + ... , then the exponent agrees with the coefficient, so that's nice...
This makes a lot of sense in the context of programming languages, where counting usually starts "where you are", thus 0 moves forward.
But it doesn't really apply in the natural world, where 1 is a much better starting number for probably all contexts where you have to number or reference things in an order.
Maybe this clarification is redundant, I don't know. But when I read the headline, I assumed it was talking about everywhere, which is why I found it interesting and even clicked it in the first place.
It does make sense in the natural world for distance and time, though. For example, when you "count to ten" you are not counting 10 seconds if you start at one, but 9 seconds, as you say "1" on second zero. To count 10 seconds, you need to start at zero ("where you are", as you mentioned). Same for distance.
I'd say that the only situation where it makes sense to count from 1 is when you are counting objects. Of course, counting objects is so important that it dominates people's perception of numbers and counting.
If you're counting off seconds, you say the number of seconds that elapsed before your declaration:
"pause one, pause two, ..."
So when you get to 10, you have said 10 numbers and there have been ten pauses. The "you are only counting 9 seconds" thing would only make sense if the counting period starts when you say the first number, which is not how I usually do it...
I agree that counting from zero has nice properties. But I must concede that e.g. in music, the surrogate for zero is the "first" beat. You are basically counting when intervals of time start, not when they end. The first second ends with the second beat (pun intended).
I typically only put the pause before one if I internally count a 0 first. :) But almost always I start the count and then pause. If I'm counting to 10, I'll start at one, and when I reach ten, still pause at the end before taking action. Similarly with counting down: if I'm counting down from 3, I don't pause, then say 3, I say 3, then pause. When coordination is needed, to reduce ambiguity I and pretty much everyone I've met say something that's not a number to signify an action starting (like "go!", or "start!"). The only other variant I've come across occasionally that puts the pause before is really just moving the non-number element to exist before the pause -- i.e. "on the count of 3 pause one, pause, two, pause, th(everyone starts)ree!".
Maybe this is culture-dependent, but at least in my environment, people who do this don't make a conscious pause before the "1". For example, when I played hide-and-seek as a kid (and when I see kids playing now), the commonly accepted rule is that you can start running when the player who counts says "1". So you effectively have 9 "time units" to hide, not 10.
Anyway, if you do make a conscious pause, I would argue that you are effectively counting from 0 even if you don't pronounce 0 due to the bias against that number caused by counting objects. The first pause needs to start somewhere, the start of that pause is in your mind (even if you don't pronounce it) and it corresponds to 0.
And nearly every mathematical context as well. There's a reason why R, Julia, Mathematica, Matlab, etc. use 1-based numbering -- it lets you translate standard formulas into code without fiddling with numbering.
It lets you translate freshman level math problems with the same indices. As soon as you deal with Fourier transforms, Vandermonde matrices, polynomials, or pretty much anything where the array index is related to the value in the array, zeros make more sense. People like 1-based because they're familiar with it, and that's fine, but pretending that it makes more sense because their familiar with some math that uses 1-based is silly.
It also makes sense when counting time, because then every timestamp is equal to duration from start (eg. it annoys me that in Bender by default the first keyframe is 1 and not 0, because if you want something that lasts 100 frames then it ends at 101 frame)
Surely that can't be true? If it lasts for one frame it ends on one, if it lasts for two frames it ends on two, ... if it lasts for 100 frames it ends in 100. Or am I missing something?
The first frame is usually not considered a frame as no animation comes from it - but starts from it. So the second frame is actually the first frame of animation. So if you want 100 frames of animation - you need 101 frames. Therefore your first frame is really "0" and you start counting (moving) from there.
I've only seen naturals start with zero in Calculus 101 books. Any "introduction to real analysis for third-year undergrads" will do 1, at least in my experience.
Despite having a math degree, somehow I never took an analysis class, so I can't common on that specifically (I do believe that's one of the places where analysis diverges from other field's conventions; that being said, I do believe it is fairly standard practice to specify whether naturals start from 0 or 1 in a paper).
In naive set theory, starting with 1 poses some problems (specifically, you don't have numbers yet, so you start with the notion of the empty set, which you use to define 0, and then proceed from there).
Starting the naturals from 0 is also problematic in group theory, since you lose the additive identity.
He argues that 1 < i … is bad because it doesn't really work if your lower bound is 0 (because then you'd write -1 < …). Funny thing is, in most programming languages, there is a biggest natural number, too. For example, in Rust, you have a problem, if you want to enumerate all possible byte values:
for i in 0u8..256 { } // <-- doesn't compile, because 265 is not a valid value for a u8
The most common case where +1 arises in 1-based indexing code is when you have to index from the end, but the language doesn't support a special syntax for it. So, for example, if you need to slice an array, starting from the Nth, and up to and including the Nth from the end, and your slice syntax is end-inclusive (as is typical for 1-based approach), you get something like:
a[N : length(a) - N + 1]
However, if you do have such special support - e.g. if negative indices are treated as from-the-end (like Python does), then it's just:
a[N : -N]
Compare this to 0-based N with end-exclusive slices (again, as is typical). If you don't have reverse indexing, it's slightly simpler:
a[N : length(a)]
but with reverse indexing (where -1 is the last element), it's actually worse:
a[N : -N - 1]
FWIW, most of it has more to do with the fact that 1) negative numbers are probably not the best way to represent reverse indices, and 2) depending on the situation, either end-inclusive or end-exclusive can be convenient.
In fact, merely having a special syntax for reverse indexing solves it for 0-based languages. For example, suppose that we extend the slice syntax such that either index can be prefixed with ~ to indicate indexing from the end. Then ~0 is the last element, and it becomes:
a[N : ~N]
This has a side benefit of avoiding errors when the computed index is accidentally negative, and ends up being treated as reverse, even though that was not actually the intent. Encoding the direction in the slice syntax itself means no runtime magic.
Alternatively, you can provide syntactic sugar for the length of the array within the indexer - say, `$`. Then, in 0-based end-exclusive approach, you just write:
a[N : $ - N]
This is one case that 1-based approach doesn't handle nearly as well, because for the same simple syntax to work there, you'd need to make $ be length + 1, which is kinda ugly.
However, this only works if your ranges aren't first-class (i.e. if they only appear inside []). These days, the trend seems to be to have ranges as first-class values in the language, and have the indexer accept a range as an operand. In that case, since the range can be constructed separately, it needs to be able to encode "index from start" and "index from end" inside the object itself, and special range syntax for that makes more sense.
If the language uses iterators or equivalent, then the iterator itself can encode the direction, and the range is just a pair of iterators. But you would still want some special short syntax, or at least a short convenience function, to easily create reverse iterators. C++ does just that - a pair of iterators expressing the same thing for random-access container `a` would be:
make_pair(a.begin() + N, a.rbegin() + N)
This is very generic, but the annoyance here is that you have to repeat `a` twice, and it could be a lengthy expression, or even have side effects. Some form of let..in syntax to introduce local bindings in expressions, like ML does, would help a lot here - you could do e.g.:
let a = <some very long expression>
in make_pair(a.begin() + N, a.rbegin() + N)
The last item of an array should be simply arr[-1]. This is very easy to understand once you have played with it for a few days.
Your second example isn’t ultimately about array indices, but is caused by confusion between using 0 as both an ordinal number and as a representation of boolean false.
The problem with this is that it breaks the conceptual model of zero-based indexing - your indices start from 0 when counting from the left, but from (-)1 when counting from the right! It always irked me in Python. This is especially painful if you need to do further arithmetic on the indices in a single slice operation, because you have to remember the different rules at play here.
With 1-based indexing, you can just skip zero, and be consistent - a[1] is the first element from the left, a[-1] is the first element from the right.
The “conceptual model of zero-based indexing” is: use the left fencepost as the index. This is consistent and easy to reason about.
With one-based indexing, you can’t have arr[-1] as the last element (that would cause huge problems!). Languages with one-based indexing write something like arr[end] or arr[arr.length] to refer to the last element, with arr[end-1] or arr[arr.length-1] referring to the second-to-last element.
C++ has arr.back() which is much nicer to read, but has the downside that behaviour is undefined when the vector is empty. Seems like interfaces always have pain points.
That whether an element was found in a list or not has nothing to do with the order relation on the type of indices. Conceptually, it's a completely distinct case; in a more solid type system it would be captured by an option type. This becomes especially evident when you attempt to generalise this searching operation to arbitrary maps.
Of course, one could argue that this just means that the length of an array is a less meaningful property than its last populated index (what Perl calls `$#array`); one can imagine
arr[arr.lastIndex]
or, which is probably better, just
arr.last
> Or a found item by index number:
if (arr.indexOf("thing") > -1) {
This comes from overloading return values to communicate errors. It's not clear to me that
if (arr.indexOf("thing") > 0) {
(in a hypothetical analogue with 1-indexed lists) demonstrates any clear improvement; something like
if (arr.member?("thing")) {
seems preferable in terms of clarity (ignoring the efficiency objection that one would probably immediately after look for the index).
As much as I respect Dijkstra, I never accepted this as something that makes sense, it is ugly remnant from older times where calculations were precious and it was considered smart to put number that you would add to base address, to calculate array address.
This is not commonly held belief. I am in minority but this is what makes sense and I always believed that computers should serve us, not the other way around.
1. Naturally, the first element in a list cardinally corresponds to 1. Contrarily, even official documentation of JavaScript has explicit disclaimers that the "first element of an array is actually at index 0" - this is easily forgotten, especially by novices, and can lead to errors.
2. Mathematically, a closed interval is properly represented in code as for(i = 1; i <= items.length; i++), because it includes its endpoints. Offset notation instead is technically a left-closed right-open interval set, represented in code as for(i = 0; i < items.length; i++). This matters because code deals with integer intervals, because all elements have a fixed size - you can not access a fractional part of an element. Integer intervals are closed intervals, thus conclusively proving this importance.
3. Mathematically, matrix notation also starts with 1.
4. The last element in a list cardinally corresponds to the length of the list, thus allowing easy access with items.length rather than having frustrating (items.length - 1) arithmetic everywhere in your code.
5. Negative indices are symmetric with positive indices. Such that -1 and 1 respectively refer to the last and first element, and in the case where there is only one item in the list, it matches the same element. This convenience allows for simple left and right access that offset notation does not provide.
6. Non existence of an element can be represented by 0, which would conveniently code elegantly as if( !items.indexOf('z') ) return;. Rather, one must decide upon whether if( items.indexOf('z') == -1 ) return; is philosophically more meaningful than if( items.indexOf('z') < 0 ) return; with offset notation despite ignoring the asymmetry of the equation.
These are excellent points. I find that many programmers will assume that zero based indexing is better because that's what was drilled into them until they stopped making the mistake and authorities in computer science (like Dijkstra) said that it's better.
ordinally, not cardinally. Ordinal means Order, Cardinal means Size.
And choosing "0" to mean "absent" is as arbitary and misguided (unless you are optimizing bits and cycles on a low-powered machine) as choosing "-1" to mean "absent"
> 1. Naturally, the first element in a list cardinally corresponds to 1. Contrarily, even official documentation of JavaScript has explicit disclaimers that the "first element of an array is actually at index 0" - this is easily forgotten, especially by novices, and can lead to errors.
'Cardinally' is wrong; it refers to cardinality, i.e. measuring the size, not indexing. It's just a historical accident that indexing traditionally started with one, and most languages lack a word for an ordinal number corresponding to zero. That's mostly because for a long time zero hasn't even been recognised as a number at all.
But the Ancient Greeks didn't recognise 1 as a number either. We've grown past that.
> 2. [...] This matters because code deals with integer intervals, because all elements have a fixed size - you can not access a fractional part of an element. Integer intervals are closed intervals, thus conclusively proving this importance.
Half-open intervals preserve the property that [a,b) ∪ [b,c) = [a,c) whether the domain is discrete or not. Half-open intervals generalise well both to empty ranges and infinite ones. In fact, in set theory ordinal numbers can be said to be defined as half-open intervals starting with zero (according to the most popular modern definition): an ordinal is simply the set of all ordinals that precede it.
The 'fractional part of an element' is nonsense.
> 3. Mathematically, matrix notation also starts with 1.
This is just a historical accident as well.
> 4. The last element in a list cardinally corresponds to the length of the list, thus allowing easy access with items.length rather than having frustrating (items.length - 1) arithmetic everywhere in your code.
That is somewhat obtuse, but I don't believe it outweighs other concerns. And again, not 'cardinally'.
> 5. Negative indices are symmetric with positive indices. Such that -1 and 1 respectively refer to the last and first element, and in the case where there is only one item in the list, it matches the same element. This convenience allows for simple left and right access that offset notation does not provide.
Zero-based indexing together with negative indexing lets you effortlessly treat any list as a circular list. Which is related to the reason why people bring up modulo all the time, by the way.
Tangentially, Lua uses this very convention (1-based indexing, closed ranges and negative indexing) in its string functions. It annoys me every time I write something parser-like by hand.
> 6. Non existence of an element can be represented by 0, which would conveniently code elegantly as if( !items.indexOf('z') ) return; Rather, one must decide upon whether if( items.indexOf('z') == -1 ) return; is philosophically more meaningful than if( items.indexOf('z') < 0 ) return; with offset notation despite ignoring the asymmetry of the equation.
The first is obviously more sensible. Failure to find an element might just as well be represented by a null; that the value chosen to represent it is an integer smaller than any valid index is mostly an accident. In a better-designed programming language, a function equivalent to indexOf would have a return type of an option type of a valid index. The language would also have pattern-matching constructs handling it with a syntax just as snappy as a boolean negation is in JavaScript. (Why? For one, because it generalises well to maps of arbitrary type.) This remark proves little apart from pointing out the shortcomings of JavaScript's type system.
Offset (distance, 0-based) and Ordinal (position, 1-based) are two different things, and the confusion disappears when your program/language properly treats different types.
I doubt it, but even if you're right, 1-based indexing wouldn't eliminate off-by-one errors. They're easy to make regardless.
Ask ten people to implement binary search with 1-based indexing, for example. Watch the off-by-ones roll in.
In fact, from https://en.m.wikipedia.org/wiki/Zero-based_numberingWith zero-based numbering, a range can be expressed as the half-open interval, [0,n), as opposed to the closed interval, [1,n]. Empty ranges, which often occur in algorithms, are tricky to express with a closed interval without resorting to obtuse conventions like [1,0]. Because of this property, zero-based indexing potentially reduces off-by-one and fencepost errors.
I find that statement very suspect, and I highly doubt there is any data to back it up. In my opinion [1,0] is far more obviously an empty range than [1,1) but that's just what it is, my opinion. And I suspect what we are seeing in that Wikipedia article is the author's opinion, not anything backed up by facts or data.
Edit: Lol, the Wikipedia article cites Dijkstra's article.
> In my opinion [1,0] is far more obviously an empty range than [1,1) but that's just what it is, my opinion.
On its own, I think that's a reasonable contention, but it doesn't work when combing intervals: [1, 1) combined with [1, 2) is still [1, 2), but [1, 0] combined with [0, 2] isn't [1, 2].
This reflects the concept of closed and open intervals in mathematics. It might have been useful when porting FORTRAN programs to Mesa. In FORTRAN, arrays start from 1. Pascal required a range in the declaration. When C started consistently from 0, that was considered radical.
For programming, as Dijkstra mentions, a consistent start from zero seemed to be helpful.
I assumed that indexing started at zero because of binary representations. For example if you're using two bits to represent four states, the first one will be 00 and the last one will be 11, which is three.
I agree with Dijkstra on using one-side open, one-side closed boundaries to denote a range of integers.
(C.S. way) 0 <= a < N
(Math way) 0 < b <= N
As he says, both ways have the property that
"upper bound - lower bound = Number of elements", both can represent an empty set "lower bound = upper bound", and both make partitioning easy "(0 <= a < M)U(M <= a < N)".
However I disagree that including the lower bound way is preferable to the starting at 1, math way. Math is the older science and children are still taught to count in this way.
As many of the benefits of one way or the other are mostly just network effects (i.e. in math subscripts typically start at 1 or in C, ints and structs are pointed to by their lowest char), we should have used the notation of the older science.
Is this whole comment train 'bike shedding'? Like tabs/spaces, brace indent levels and all the others, these arguments and explanations about why one way is better is like watching people run around a tree.
Bike shedding as I understand it is more about non-experts arguing endlessly about some trivial detail because everyone can understand it and has an opinion.
What's happening here is experts arguing about minutia that feels important to them, but looks trivial from the outside.
There is a certain elegance tos tarting at zero, most easily appreciated when writing assembler. And I agree with dijkstra that Adhering to convention a) yields, when starting with subscript 1, the subscript range 1 ≤ i < N+1; starting with 0, however, gives the nicer range 0 ≤ i < N.
Such minimalism is elegant and efficient, to be sure, and I'm a big believer that beauty often shows the path to truth. So from a computer science standpoint, I wholly agree.
From a software engineering standpoint it's really fucking stupid. People generally learn to count on their fingers and they need to grasp the concept of counting and numbers as an abstraction before they can get with the concept of a zero.
Counting on your fingers is one of those very basic things like learning the alphabet (ro its equivalent in other languages) or learning to tie shoelaces. Nobody I have ever met defaults to counting from zero, not even people who claim to do so - easily tested by buying them a drink at some later time and tricking them into counting something.
If you're a software engineer your job is to build something that works and is maintainable. This is not the same thing as doing computer science or mathematical computing, even though it may all be math from the computer's point of view - just as architects are not mathematicians despite their reliance on geometry. Array bound errors are omnipresent in software projects because counting everything from zero is directly at odds with how we count things in the real world, and when we're in a hurry or under pressure we default to learned behaviors. Even people who have been bilingual for years will burst into their default language or switch between two default languages when they're excited. Building your code to go from 1 to n+1 may not be quite as beautiful, but it is a hell of a lot easier for other people to read and (in my experience) you make fewer mistakes if you switch away from starting at zero.
Yes, this requires developing some new habits that feel really awkward at first. Since I've only ever programmed for myself rather than as part of a team I haven't had to deal with the inevitable resistance to this. But it is worth making the switch. Just as a lab and a construction site are very different environments [1], so are the practices of computer science and commercial software development. It would also make a huge difference in programming education, where many prospective students are alienated by being asked to do something highly counterintuitive very early on (typically while trying to grasp the concept of looping) and lead to errors which they are likely to keep making forever because nobody defaults to counting from zero in any other context.
If you're a computer scientist, carry on as you were. If you're an engineer, then build for the needs and instincts of your end users - some of whom will eventually be programmers - and not for the computer gods. They're not going to be around to help when your code breaks down.
I'm not very optimistic about this plea (especially not in the USA where y'all won't even adopt the damn metric system), but please at least give it a try. Technology should adapt to the needs of the people that use it, rather than the other way around. If you're working in a high level language, then use high level concepts.
1. Remember the story about the three little pigs who all built their houses out of subatomic particles formed into atoms formed into molecules and whose houses were topologically identical but had different coefficients of structural stability? Me neither.
I've spent years writing in 0-origin languages and years writing in 1-origin languages. I do not agree with your assertion that 0-origin is more error-prone; quite the contrary. If you're manipulating sequences using indexing, there are more cases in a 1-origin system where you have to remember to add or subtract 1; a 0-origin system has fewer of these, and if you learn good habits, you can get rid of almost all of them.
(And for the record, I started out in the 1-origin Basic and Fortran world; my preference isn't just a matter of which one I learned first.)
I started out with 0-origin and for pure programming I agree. The problem is that real-world indices almost invariably start at 1 and that is how everyone who isn't a programmer counts, so automating anything based on a real-world process requires a translation. Conversely, when testing and debugging against real-world stuff everything is going to be off by one. It's just asking for trouble.
Sometimes the only way, for people to take your ideas serious, is to get them to want to take them apart. 500 pages of proof, but to silence this grating, always arrogant, ####### at the next conference, so worth it.
Surely not the Noble kind of sciences, but if animosity leads too thoroughly read papers and found errors, maybe the dark side is just willing to work harder.
Also, the recent rise in programming with multidimensional data invites the question of why matrix integer indices should include zero, which Dijkstra's essay doesn't address.
Likewise, the increasing use of real variables in software invites asking whether an elegant syntax for specifying integer ranges applies equally well when used for real ranges, since <= operators make less sense when constraining floats. Also not addressed.