One thing I would hope there is consensus on is that 1-based indexing is easier to learn. My son is doing Python at school, and the "first-element-is-actually-element-zero" is something that has to be dinned into them, a sure sign that it is non-intuitive. In similar vein, as adult programmers we know that the half-open range is the most useful mechanism for expressing a sequence, but again we have to explain to kids that if they want the numbers 1 to 12 (eg to do a times table program) they must type range(1,13), which at that stage of their learning just seems bizarre. Actually I could go on at length about why Python is a terrible teaching language, but I'll stop there !
It is non-intuitive, but it doesn't happen only in programming. Look at sports reporting, for example (basketball, football). When they say something happened "in the 6th minute", it happened between 05:00 and 05:59. The difficulty isn't solely with programming.
You can use the same mechanism (minutes:seconds) to explain the half-open ranges: does a range(0,30) mean from 0:00 to 30:00 or from 0:00 to 30:59? If the second half of the match starts at 30:00, does the first half really end at 30:00 or is it really 29:59:99...9?
For most basic concepts, there's always real-world examples to use so that the teaching doesn't have to be abstract.
There's another non-intuitive and also inconsistent usage of ranges that I find rather confusing: if you say that a meeting is from 9 to 10, it's one hour long. If you say that you'll be on vacation from January 20 to 21, most people seem to think that means two days.
That's because hours are announced as points in time, while days are seen as time intervals. The duality
intervals/separators is essential to the different ways to understand ranges (specially for range differences).
I once developed a shortcut notation to simplify reasoning about ranges differentiating between "point cero" and "interval between 0 to 1", to make off-by-one errors less likely, but in the end it was functionally equivalent to the popular open/closed interval notations.
Specifying vacation range got me in to few confusing situations, so I have a habit now announcing them as: "I'm on vacation from X to Y, returning to the office on Z"...
1 hour meeting -> meeting start at 9 and is expected to finish when it’s no longer 9
1 day vacation -> vacation start on January 20 and is expected to finish when it’s no longer January 20.
Ultimately, it relies on conventions, but I guess now you can see how this one relies on something which has some consistent pattern.
I thought the proper way to say this is January 20 through 21, but seems like most people don't follow this. Probably because it's often abbreviated as 20-21, which is often used for both cases.
Time is a continuous variable, and kids know this pretty early on: they will happily tell you they are 5 and a half years old. Their second year starts when they are aged 1.0.
English in general makes this distinction: "He is fewer than 6 years old" is simply a mistake, while "less than 6 years old" is fine. (I think some dialects use "fewer" less often, in general, but would be pretty surprised if any prefer it here.)
To be pedantic, it's the first annual celebration of your birthday.
Other languages use a less confusing term for it: anniversaire (French) is just anniversary, verjaardag (Dutch) roughly means year-rollover-day, and cumpleaños (Spanish) signifies completion of another year.
I just realized something: anniversaire has the same root as the other examples I gave. It comes from the latin words annus and verso, which mean "year" and "to turn over" respectively.
late 14c., from Old English byrddæg, "anniversary or celebration of one's birth" (at first usually a king or saint); see birth (n.) + day. Meaning "day on which one is born" is from 1570s. Birthnight is attested from 1620s.
https://www.etymonline.com/word/birthday#etymonline_v_27158
In korea, they'd be age 2. There (and possibly other east asian nations?) count "age" in terms of which year you're in. 1st year, 2nd year, 3rd year, etc. The way americans do school year, except it starts at birth.
When my korean friend first told me this, I honestly thought they were messing with me. I still struggle to make sense of it. Must be a helluva birthday party on Jan 1st tho
I know there are studies on how birth date affects future success in school (though IIRC I read in Gladwell, and I'm wary of accurate his read on things are), I wonder if that effect is compounded with this system?
What we call “age” is merely the number of the most recent birth anniversary we’ve passed. That makes sense. While your take isn’t invalid, it sets up a less intuitive construct that isn’t aligned with the common meaning of the word.
You're supposed to start out by teaching them pointers. Once they fully understand how pointers work, you move them on to Hello World so they can write their first program. Then it will be intuitive when they see 0-based indexing. I think that's the HN consensus on teaching kids how to program.
That's an excellent idea! We should also teach them quantum physics so instead of questioning programing language flaws they would question reality. Another problem solved.
I think we are on the track to create a perfect programmers factory.
Honestly a lot of the confusion that I've seen in novice programmers over the years comes from the fact that nobody explained to them (1) that a program is a set of instructions that are executed one after the other by the computer, and/or (2) that memory is like a long line of boxes, each of which can hold a byte.
Once someone understands (2), pointers are incredibly simple and obvious.
I have been surprised several times, when helping people to write "their first program" (which, in science, isn't that rare), that it can take days to internalize the relevance of ordering statements in a program. It's deep in every coder's subconscious that the bottom line depends on the top one and not the other way around, and I have had to consciously restrain myself from saying "whaaaaat?!" when I saw how my colleague tried to do something.
I'm glad you've learned to restrain yourself because that mistake makes sense if you're not used to imperative thinking. It doesn't benefit students to have instructors who are shocked by common/natural errors.
Math, as commonly encountered, is a collection of facts and relations. Their notion of variable is very different than in CS and programming.
To a mather (one who uses/does math), this makes sense:
x = 7 * y
y = 6
therefore x = 42
To a coder (of most conventional programming languages) that's an error, y is undefined when x is assigned a value. And even if it did have a value, changing it later wouldn't change x so who knows what x could be.
It makes sense to the mather because each statement exists at the same time. To the coder, each statement is realized over a period of time. To the coder "y = 6" hasn't happened yet when "x = 7 * y". This is the part that has to be communicated to the novice with a mathematical background.
There are non-imperative logic programming languages (Prolog) where something like that would work. And those of us who started with imperative coding are probably just as confused by them in the other direction.
That's why I qualified the statement with "conventional". Prolog is not a conventional language, in the sense that most programmers don't know it (and I'd wager close to half of programmers have never even heard of it, if not more). C, C++, Java, JavaScript, PHP, Python, Perl, Ruby, even Rust are all imperative languages at their core and represent the dominant group of languages in conventional use.
If the OP was teaching people a language like Prolog then there wouldn't have been the same confusion.
I actually started writing a comment about that. Teaching VHDL (my experience) to most programmers is an entertaining experience because they expect effects to be immediate, not executed collectively and simultaneously but mediated by a clock or other mechanism. So in an HDL:
x <= y;
y <= y * 20;
These two effects actually occur simultaneously, which is different again from what mathematically inclined learners might expect since there is still mutation. It is more akin to the common notation:
x' = y
y' = y * 20
Where the ' indicates the next value of x or y (as opposed to the derivative). Or perhaps:
x_n = y_{n-1}
y_n = y_{n-1} * 20
And many programming languages don't even have a good notion for similar parallel evaluation and assignment. Python does, with:
x, y = y, y * 20
Common Lisp has psetf which permits this:
(psetf x y
y (* y 20))
Languages with pattern matching/destructuring bind are your best bet for writing this directly without needing temporary variables.
What the typical programmer doesn't even realize is that this is an artifact of Von-Neumann architectures (and their assembler language) and optimization of early compilers, not a essential property of computation.
Spreadsheets, expression-rewriting languages or any other functional programming language don't necessarily have this limitation, and declarations can be made in any order if the compiler supports it.
I've noticed this as well, and I've concluded that the actual surprising part for new coders is mutability. In basically every domain where non-developers invent algorithms, everything is immutable - spreadsheets, math!. Heck, even reading an article - the words you've already read don't change their meaning just because you've kept reading.
Mutability is really a result of the fact that computers are physical machines. So I think if you're going to teach software development, you should really start at the machine architecture and build up (ala NAND to Tetris) or start with an immutable-by-default language and only dip into mutation as an "advanced" concept, only to be used by experts and definitely not by beginners.
I endorse the second way, as I've seen it work very well, whereas I've seen more than one ship crash on the shore of understanding pointers...
I once had the surprising experience of having to help a postdoc with a PhD in mechanical engineering wrap his head around a block of Python code he was reading that looked like:
x = f0()
x = f1(x)
He was just mentally blocked on how all of these things could be true at the same time, and/or on how a variable could be used in setting its own value, since assigning a new value to x would change the value that had been passed to f1! His intuition was that this meant some kind of recursion-to-stability was at play.
All he needed was to be informed that this could be equivalently represented as
Someone I knew at university, who prefers starting at 0 (for mathematical, rather than computational, reasons) made the following argument/proposal.
"First" is an abbreviated form of "foremost". So if you start counting at 0, "first" corresponds to index 0. There's no real clash here yet; "first" and "one" are entirely separate terms.
"Second" is from Latin "secundus" meaning "following"; the second thing follows the first. So "second" corresponds to index 1. Again, no clash yet; "second" and "two" are basically unrelated.
"Third", though, actually derives from "three". So "third" has to correspond to index 3.
This leaves a gap, which might perhaps be filled by a new word "twifth", corresponding to index 2.
So now we have the cardinal numbers: zero, one, two, three. And the corresponding ordinals: first, second, twifth, third.
The hypothetical student's sarcastic answer isn't technically correct.
> you can just show them a ruler and
> ask them where does the first centimeter start(s)
The question involves a nuance of the english language, the distinction between a measure of wholeness and the contents of that unit.
Length is a count of the completeness of a measure. The length of an interval must inherently include 0 as a unit to describe taking no units.
A 0 starting index is likewise also required to indicate skipping no storage cells.
Therefore an absolute index start must be zero based, and the Addition of duration must also include the option of zero as a means of describing a point at which to begin, but having not yet used any length.
The subtle trick is that is a point, an array of zero size; this is why the length, or alternately absolute stop point, for an array storing anything is one higher. It isn't actually zero indexed, it's a different property of measure (the size or stopping address).
A centimeter is also a wonderful example since it should graphically contain eight additional ticks for millimeter marks, and naught else, among it's inner span. This aligns wonderfully with explaining that there are targets spots for 8 bits of an octet within that single byte of indexed storage.
It's the offset of the first array element from the start of the array. The first element is at the start of the array, so the offset is naturally zero—there are zero elements prior to the first element. Like the markings on a ruler, array indices are cardinal numerals (0, 1, 2, …) counting the distance from a reference point, not ordinal numerals (1st, 2nd, 3rd, …) representing rank in a sequential order.
If you're comparing arrays to centimeters on a ruler, the first element is at the center of the first cell; at zero, there's the left border of the first cell.
(Also please note that both this and my previous reply are tongue-in-cheek).
There's a difference between a cm being the thing that exists between the lines with 0 and 1 against them and the distance that is 1cm. Indexing is within the scope of the first definition. You might as well ask where the 0 or 1th element in an array starts and finishes.
Imperial, because of it's vagueness people use other measurements as well.
Something is x stories high.
Something is a "stones throw away".
Though i do appreciate Fahrenheit for how it scales with humans. 0 is really damn cold, 100 is really damn hot, while in Celcius 0 is kinda chilly and 100 is instant death.
It's actually supposed to be the body temperature. Due to imprecise measurements, it's actually having a light fever. Flip note: C scales the same - 0 is the water freezing point, and then you have the same scaling as F but in 5s instead of 10s. [and you gotta remember: (f-2pow5)*5/9].
That's an old debate, yet if you are born/used to C, F is pretty horrific.
I agree, but Python doesn't have pointers. So I agree with your parent that Python isn't a good language for teaching.
Unfortunately teaching languages that are good for learning, like Scheme or Pascal, went out of fashion a long time ago. For years now universities have been teaching marketable languages. Ten years ago that was Java. Now it's Python.
It's hilarious that people are taking this comment literally. HN comedy gold. Reminds me of when I was working with a German work college and he just could not pick up on English sarcasm.
There's two groups of programmers, those who started learning with low-level, and those who started with high-level languages. I'm personally in the former group, and think there are definite advantages to doing it that way. You are probably in the latter.
Sure, for adults, that's fine (but I still disagree with the approach, as pointers are a notably tricky concept that really isn't necessary in the beginning). But it's like saying that before teaching a kid addition and multiplication they should understand the Dedekind–Peano axioms -- it's simply overkill and more likely to discourage them than anything else. (But it depends on age and aptitude, of course, so obviously YMMV).
FWIW I started with K&R, but times have changed and I definitely don't think it's a pedagogically sound way of teaching programming.
Back in the mid-to-late 20th century, there was a movement to teach mathematics rather that way, though I don't think it ever got as far as teaching small children the Peano axioms. The "New Mathematics", they called it. I remember encountering talk of sets and functions in UK primary school circa 1975.
It never worked very well in practice. I expect that when you're fortunate enough to have unusually able teachers teaching unusually able pupils it might do OK -- but then, in that situation, anything will do OK.
(Tom Lehrer fans will know about this from his song "New Math", with "It's so simple, so very simple, that only a child can do it" in the refrain.)
For the record: Lehrer-style "new math" was what I was taught at elementary school in Ontario in the 1980s. I know it was not what my parents had been taught, because they couldn't understand how we did subtraction. We did do some other bases (mainly binary, as I recall) as well as modulus.
I don't think it's comparable to teaching arithmetic from a highly theoretical basis, since the type of low-level beginnings I'm advocating for are starting from a concrete foundation --- logic gates from relays and such, not unlike the approach used by Petzold's book Code.
Incidentally, that book has been praised for making computation easy to understand for non-programmers, which I would consider to be evidence for starting out low-level.
but times have changed
Not for the better, if you look at the average state of software today --- and I think a lot of it can be explained by the general lack of low-level knowledge.
A pointer (or reference, or just 'address') is a general concept that by definition is part of any language featuring mutation.
Any language that makes use of mutation for primitive constructs (not saying that's the case here specifically) needs an understanding of "observing/causing change" pretty early on.
> is a general concept that by definition is part of any language featuring mutation.
One counter-example is Mathematica. I can mutate any object any time I like, and doing so is extremely common. Yet under "pointer" its help tells me about some unicode symbols, and some GUI stuff about mouse pointers.
In reality, some of its lists are dense memory like C, some contain arbitrary objects (such as expressions, or high-precision numbers), but these implementation details aren't part of the language, and most users never know them.
Mathematica is also not a low-level programming language. It's math-based, so it deals with abstract concepts, not concrete hardware implementations.
0-based arrays are based on hardware implementations; 1-based arrays are based on abstract collections; you could make the argument that 0-based arrays are a leaky abstraction, but the question then becomes: what abstraction? C explicitly tried not to abstract away the underlying hardware.
If "any language" means "any language sufficiently low-level to have C-like pointers", then no contest!
Surely the point of any language at all is to abstract away some details of the underlying hardware. Both for portability and to make things easier for human brains. How much you should abstract away obviously varies, based on what you're trying to do, and on how varied the hardware is.
And it depends on how much computation & memory you can give your compiler. One reason new languages can have nicer abstractions is that they don't have to compile on 1970s hardware.
0-based arrays/indexes are also based on abstract concepts. 1-based ordinal numbers (in both maths and language) are just tradition from time before 0 was generally accepted. Set theory (as a clean design of math foundations) also starts ordinals at 0.
The specific word or implementation isn't important.
In Mathematica's case (and most other languages, sadly), presumably the concept of 'variable' is overloaded with the semantics of a pointer/reference. Meaning, you cause and observe changes to locations via a variable.
At this point even assembly language is an abstraction of a simpler machine overtop of a much more complicated one, and few programmers really understand what's happening underneath. It's not like a chunk of 'memory' in your C program corresponds exactly to a physical location on a DRAM chip anymore. There's N layers in-between, not just in the OS's virtual memory model but also in the chip's caches, etc.
We're all dealing with abstractions over abstractions at this point, so it's really about finding what model of understanding works for the learner and then going from there.
Terrible analogy. The beauty about computer science lies in abstractions. You don’t have to understand the low level details to be productive at the higher levels, you just have to understand the interfaces. If we go down this rabbit hole, do you have to understand how the memory bus works, how the operating system is implemented, etc... before getting to hello world?
There is a huge difference between explaining how indexes come from offsets and understanding operating systems. I think more emphasis should be put on the insane amounts abstractions underlying the high level language at hand when teaching programming. Explaining that 0-based indexing comes from offests seems like a good start in that direction for a kid learning python.
And when beginners won't be beginners anymore they'l have to learn how the underlying abstractions work anyway.
A programmer that doesn't at least superficially understand how operating systems work is not going to be able to write performant or secure software, let alome combine those qualities.
You don’t have to understand the low level details to be productive at the higher levels, you just have to understand the interfaces.
I've worked with programmers like that. They can glue a lot of things together, but when the slightest of problems appears, they are absolutely lost with how to debug. They usually resort to making pseudorandom changes until it "appears to work", and are also unable to appreciate just how fast computers are supposed to be.
I learned physics and transistors at comp-sci. It didn't help with programming at all (though it made wonders for my understanding of the philosophy of computation).
The kind of understanding you're rooting for relies on formal logic, not electronics.
That depends entirely on the kind of programming you're trying to do. For beginners working in high level languages it is an implementation detail they need not be concerned about.
Growing up with BASIC, I didn't have any understanding of pointers or how memory worked. PEEK and POKE, which I only saw used in other people's programs and examples, were black magic as far as I could tell. It wasn't until I learned C that I actually understood them but I still managed to write a whole lot of programs without ever using them.
To be a professional programmer I'd agree though. If you don't understand how pointers work, regardless of the language you're using, you're probably not good at your job.
Until you need to substitute ingredients, you use a different type or rangetop, the pan metal or thickness is different, or something else changes. This was a perfect analogy. People can follow the recipe of assemling various frameworks and toolkits to create software, but they will eventually encounter difficulty.
You may not need to understand molecular differences, but knowing hows fats, proteins, and carbs work, along with how to substitute ingredients, and different theories of cooking all help to actually understand why you can cook until mysteriously things don't work the same as yesterday.
It is said that each fold on a chef's hat represents a different way they know how to cook an egg. Thet need to understand when to apply low heat, high heat, add liquids, when proteins denature and when they coagulate.
Of course, following a recipe in specific ideal circumstances works, as long as nothing changes.
That's how they used to do it in universities back in the 80s/90s. Start out with the hard stuff first in Intro to Computer Science. By the end of the course, you'd go from 400 people to about 50. Wish they'd still do that to keep numbers low.
Although doing it "to keep numbers low" doesn't seem to me to be a good reason to do it, it is generally a good idea to have some hard stuff early in a field.
The idea is that if the hard stuff comes too late, people might not find out that they aren't really suited to that field as a career until it is too late to change majors without having to take an extra year or two of college.
The intro class is too early for that, though, because CS is useful enough for other fields that many people (perhaps even a majority of people) taking intro CS don't intended to actually go into CS. Better is to put it in the first class in a field that will mostly only be taken by people who are intending to go into that field.
The best college courses I took were ones where the professor frontloaded all the hard assignments towards the first month of the semester. Every slacker dropped out, and for most of the semester we just had great discussions because everyone was doing the readings.
Just like DRM, make it a little harder to skate on by and you actually filter a lot of casual/non-serious people.
You are going to get downvoted to hell for "gatekeeping" but you're right.
Instead of just graduating 50 people who "get the trade", we're now diluting them with another 350 that "made the grade". Then tech firms need wild, crazy interview processes to sort them back out. Everybody suffers, including the 350 who may have been productive and happy in another, more suitable profession.
And it's weird that people see it as gatekeeping, since there's plenty of other professions out there that artificially restrict supply, and no one raises a sthink about them. People do about software because we work online and are constantly giving opinions no one asked for.
It's less about gatekeeping, and more about sustaining a healthy industry that is quickly headed for a disastrous race to the bottom. Google and friends are spending billions under the magnanimous "bringing coding to everyone" when they really just want to lower developer salaries. Not everyone needs to learn how to code, just like not everyone needs to be a doctor, or learn how to be a carpenter. I constantly wonder when will be the right time to "get out" before salaries start their inevitable side downwards. Could be a couple decades though.
Honestly, the problem is that we don't distinguish cardinal from ordinal numbers in programming
As an offset (which is cardinal—how far something is from a starting point) or really any cardinal use, zero is the natural starring point. For ordinals, “1st” is the natural starring point. But we don't write “1st” in programming, even when we used 1-based indexing to try to match an ordinal intuition, we just write “1”. If we distiguished ordinals and allowed both cardinal offsets and ordinal indexes to be used, I think we’d be fine.
The unintuitive step is apparently identifying the index with the cardinality of the collection from before the item arrives, not after it arrives – i.e. ‘the n-th element is the one that arrived after I had n elements’, not ‘the n-th element is the one such that I had n elements after it arrived’. The former identification results in 0-based ordinals, the latter leads to 1-based ordinals – and to some misconceptions about infinity, such as imagining an element ‘at index infinity’ in an infinite list, where no such need to exist.
It's only an ordinal because we've confused ourselves into saying it can be. It's the same situation as with starting to count from zero in most programming languages, and from the wikipedia page for "0th", it appears to actually be the effect of an influence of those artificial language choices onto the mathematical concept of ordinal. After all, it is in the end just a convention, and we could start calling the first element of a sequence "the zeroth" instead, but there would be no benefit for it at the language level - which is where it matters. These artificial languages we're creating should try to mold themselves to how we already use language, but the opposte is what's been happening to some degree.
"1st" is not a "natural" choice for the starting point of ordinal numbers.
If anything, it is an artificial choice, because in programming it is derived from a property of most, probably of all, human languages that have ordinal numerals.
In most, probably in all, human languages the ordinal numerals are derived from the cardinal numerals through the intermediate of the counting sequence 1, 2, 3, ...
When cardinal numbers appeared, their initial use was only to communicate how many elements are in a set, which was established by counting 1, 2, 3, ...
Later people realized that they can refer to an element of a sequence by using the number reached at that element when counting the elements of the sequence, so the ordinal numerals appeared, being derived from the cardinals by applying some modifier.
So any discussion about whether 1 is more natural than 0 as the starting index goes back to whether 1 is more natural as the starting point of the counting sequence.
All human languages have words for expressing zero as the number of elements of a set, but the speakers of ancient languages did not consider 0 as a number, mainly because it was not obtained by counting.
There was no need to count the elements of an empty set, you just looked at it and it was obvious that the number was 0.
Counting with the traditional sequence can be interpreted as looking at the sequence in front of you, pointing at the right of an element and saying how many elements are at the left of your hand, then moving your hand one position to the right.
It is equally possible to count by looking at the sequence in front of you, pointing at the left of an element and saying how many elements are at the left of your hand, then moving your hand one position to the right.
In the second variant, the counting sequence becomes 0, 1, 2, 3, ...
The human languages do not use the second variant for 2 reasons, one reason is that zero was not perceived as having the same nature as the other cardinal numbers and the other reason is that the second variant has 1 extra step, which is not needed, because when looking at a set with 1 element, it is obvious that the number of elements is 1, without counting.
So neither 0 or 1 is a more "natural" choice for starting counting, but 1 is more economical when the counting is done by humans.
When the counting is done by machines, 0 as the starting point is slightly more economical, because it can be simpler to initialize all the bits or digits of an electronic or mechanical counter to the same value for 0, than initializing them to different values, for 1.
While 1 was a more economical choice for humans counting sheep, 0 is a choice that is always slightly simpler, both for hardware and software implementations and for programmers, who are less likely to do "off by one" errors when using 0-based indices, because many index-computing formulas, especially in multi-dimensional arrays, become a little simpler.
In conclusion, the choice between 0 and 1 never has anything to do with "naturalness", but it should always be based on efficiency and simplicity.
I prefer 0, even if I have programmed for many years in 1-based languages, like Fortran & Basic, before first using the 0-based C and the other languages influenced by it.
I think you're conflating "natural" with "familiar".
Here are two processes Timmy could use to count the pens on his desk. (I have numbered them starting with 1, because I considerately adapt to the needs of my audience :-).)
1. Say "0". Then increment for each object. So he goes: zero, (points to pen) one, (points to pen) two, (points to pen) three.
The advantage of this is that you don't need a special case when the number of pens turns out to be zero. Hey, Timmy, how many unicorns on your desk? Timmy starts by saying "zero", looks around, no unicorns, finished: it's the same process as for pens, it just stopped earlier.
2. Number the objects from 0, and then the count is the next number after the ones you listed. So he goes: (points to pen) zero, (points to pen) one, (points to pen) two, so the number is three.
This corresponds more closely to how indexing works in computers, and matches up nicely with the so-called von Neumann construction in mathematics, but in other ways I find it less natural than #1. But I am confident that you could teach it to children, and they would find it natural, and think it weird to mix up "the number of objects" with "the number of the last object". In this case, the only thing that changes from your dialogue is that Timmy says "Well, since the next number is three, it totally makes sense to announce that I have three pens." You count the pens, then you say how many there are. Zero, one, two, three. Not zero, one, two, two as you would prefer. What are you, some kind of weirdo or something? :-)
I did not explain in detail how a human should count, because I think that for a programmer it should have been clear that the exit criterion from the loop is in both cases to have no elements at the right of your hand.
So your Timmy has a bug in his loop code, which caused a premature exit :-)
He should have counted 0, 1, 2, 3.
Like I have said, humans do not count like this, because there are 4 steps instead of 3 steps.
For humans, it is easier to have an "if" before the loop, selecting between the special case of an empty set and a non-empty set, whose elements must be counted.
Nonetheless, an electronic counter would have really counted 0, 1, 2, 3, unlike you.
You wrote a lengthy post trying to explain how it could totally be possible that humans start counting at zero, yet the whole premise of it is wrong: we start counting at 1 because it's natural. You cannot assume otherwise.
> He should have counted 0, 1, 2, 3
Is that how _you_ count? You imagine some pointer going _between_ items, start at zero, and stop once you have nothing at the right of your pointer?
Or, little Timmy could also skip the "zero" part (because you know, he's not dumb nor blind, and sees that he has several pens on his desk), start at 1 while pointing _at_ the first pen, and count up to 3.
Pointing directly to the objects that you are counting is not natural, it is just a bad habit.
People who are interrupted while they are counting and who have to restart the counting later, frequently forget whether they have already counted the pointed object, or not.
Pointing between the objects avoids these errors.
When I was young, I also had the impression that counting from 1 is natural. When I first encountered 0-based languages, I was annoyed by them, especially because I knew how Fortran compilers are implemented and I was familiar with all the tricks that are used by compilers to minimize the extra operations that are required to work with 1-based arrays.
So my initial naive thought was that maybe the implementers of C were not competent enough. However, after many extra years of experience, I believe that all the tricks for the efficient implementation of 1-based arrays are not worthwhile, because even for programmers it is more efficient to think using 0-based indices.
So now I really consider 0, 1, 2, 3 ... as a more "natural" counting sequence, unlike when I was a child.
You don't need to start at 0 to count intervals between objects, it's just easier to say "one" and point your finger to the space to the right of the leftmost item.
> … and point your finger to the space to the right of the leftmost item.
What if there is no leftmost item? Because you skipped the first step, essentially assuming that there was at least one item and including it in your initial condition, that process fails when the set of objects to be counted is empty. Humans are good at dealing with special cases like this; they'll notice the discrepancy and report "zero" instead of following through with the predetermined process. Computers, not so much. So instead we start with a count of zero and all items in the as-yet-uncounted set as our initial condition, and increment the count by one as we move each item from the "uncounted" set to the "counted" set, which correctly handles the case of the empty set.
This whole subthread was about what comes as a natural procedure to humans. Arguing that computers may need a different process is moot.
Anyway, to handle the exception, you could start the algorithm with a "check for the empty collection" base case; that would make it a natural definition for a computational recursive definition as well.
My point was that the natural procedure for computers is also more natural for humans, since it involves fewer special cases. Humans are better than computers at dealing with ad-hoc special cases but that doesn't make them natural.
(And then, having pointed at the pens, Timmy got confused and tried to write down his answer using his finger. Annoyed that no actual writing occurred, he cut off his finger and threw it away. The next day, he was again confused to find his desk all cluttered with crap that somewhat resembled pens, despite having so bloodily thrown away his pointer.)
I doubt that cardinals came before ordinals. To me, at least at first glance, they both seem equally conceptually primitive.
As far as efficiency, this should be the task of the compiler. We shouldn't have to start counting with the "zeroth" just to save the compiler a few cycles (or a few mental cycles to whomever writes the compiler/interpreter). That's why we don't program in assembly after all.
But I agree with GP, we shouldn't start counting from 1 either, but from "1st". Keeping ordinal separate from cardinals would be closest to how natural language works and I bet it would eliminate all kind of errors and gotchas.
Well, 0-based is not what we use in everyday life, and even arriving at the invention of 0 took us several millenia, so I'd say its non-intuitiveness is pretty much established...
The ancient Greeks didn't think 1 was a number; as late as ~1500CE Isidore of Seville was saying "one is the seed of number but not number". After all, if there's only one thing you don't need to think of it as a set of things, so there's no counting to do.
(I think in some quarters it was even argued that two isn't a number, but I forget the details. Compare the fact that some languages treat pairs of things differently in their syntax from larger groups.)
But it turns out that mathematically it's a really good idea to count 1 as a number and allow sets of size 1, and these days everyone does it and no one is very confused by it. (Well, maybe by the "sets of size 1" thing. Exhibit A: Perl. Exhibit B: Matlab.) And because we're now all used to thinking of 1 as a number, it's hard even to understand how anyone might have found it unintuitive.
If we all started counting from zero and taught our children to do likewise, intuitions 50 years from now might be somewhat different from what they are now.
You didn't have 1 year, but you had 1 day old or 2 weeks old, or 3-6 months old, etc.
People never said and still dont' say "this baby is 0 years old". They start at the first complete item of the lower level time units and progress from there...
For ages people use the unit that best captures the spirit of what they want to communicate.
So they may say one hour old, five days old, six months old, etc. a fifty year old typically doesn’t say they are a half centenarian either. Now decades are used to express an age range like so and so is a nonagenarian to avoid being specific.
If you ask kids how old they are, you will often get "5 and a half" etc. They are, correctly, thinking of time as a continuous variable. You "turn 1" when your age reaches 1.0.
The main argument I have against 1-based indexing is that it makes a lot of indexing calculations, which all programmers will need to learn at some point, even more confusing with all the additional +/-1. In other words, 1-based is easier only for the easy cases, but harder for the hard cases.
In some ways I think it's similar to the endianness debate: big endian looks natural at first glance, but when you see the expression for the total value, having the place value increase with the address just makes far more sense since it eliminates the extra length-dependent term and subtraction.
I feel like it's nim that made the bad choice here. Not because one meaning of .. naturally makes more sense than another but because it breaks the expectations set by other languages.
Ada is actually not 1-based. Arrays are accessed using an index type which is defined at declaration. It can be any discrete type, including ranges starting at 0 (or 1, or -1, or anything else which fits the domain).
Interesting! I looked at a few different sites for examples of arrays in Ada and saw them all starting at 1. But that's a neat feature that it lets you pick the start and end.
Ada isn't even 1-based, though many of its built-in array-based types are 1-based. It's "whatever ascending, consecutive range you want"-based. Which, IMHO, is the best option. You can use 0-based where it makes sense and 1-based where it makes sense, and -100 based where it makes sense, or 'a'..'z' based where it makes sense.
And I think that last example demonstrates why the closed range notation makes a lot of sense over half-open (at least for languages that permit describing ranges of arbitrary discrete types). When talking about numbers a half-open range notation is clear and useful because numbers exist in an obvious sequence (so long as we're sticking to some subset of the real numbers, complex numbers don't have as obvious an ordering). But when talking about other ranges, like characters, how would you use a half-open range (in a clear way) to describe the characters a-z? 'a'..'{'? Is that actually clear?
What about describing the range when you want to include the last valid value? How do I describe, using the byte type itself, a range 0 through 255:
type Byte is mod 256;
subtype Full_Range is Byte range 0..255; -- valid Ada, closed range notation
subtype Full_Range is Byte range 0..256; -- invalid Ada, half-open notation
I'm now describing my range of a specific type using an element that isn't part of the type. For numbers this can work. But what is the character after the last character of the set of characters?
subtype Characters_After_And_Including_Lower_A is Character range 'a'..???;
-- presumably we'd have a better name in a real application
In a half-open notation that's impossible to write and be explicit about the upper bound. You can only describe it with an implicit upper bound by leaving the ??? blank as in:
subtype Characters_After_And_Including_Lower_A is Character range 'a'..;
A valid solution. Though in Ada this would be a no-go as it leaves open some ambiguity/possibility for error in the use of the `..` notation (what if the author didn't mean for it to run to the end but forgot to type the upper bound?), so a second notation would have to be formed. Perhaps a third `.` or some symbol indicating that it's open-ended:
Citing Rust is cheating - we know that it does everything in the most elegant manner. A bit like deriding the other kids in the class because they're not as good as the star pupil :)
This might be an unpopular opinion, but I'd say it's more natural for kids to relate the word 'three' to 'third' and 'four' to 'fourth', instead of 'two' to 'third' and 'three' to 'fourth'.
Yes, that's why its hard to explain to kids that a two-year-old is in their third year of life. By the time of their third year of school (second grade) they usually get it, though.
We should be careful not let such easily observed facts inhibit our efforts to dumb down curriculum.
Kids are taught units, so they express themselves properly and are understood by others. e.g. The third item over here is at a distance of two cm from the left edge.
I've always found the English language to simply be lacking here. In for instance Dutch there is a very clear distinction between half-open and closed intervals: "x tot y" is always half-open, "x tot en met y" is always closed. Whereas in English, "x until y" can mean both things, with all the resulting misunderstandings.
We have "to" and "through" in English to distinguish them. Unfortunately "to" isn't always as strict as I'd like...
"Until" I'd think of as being the strict "to", except something about it feels off like it couldn't just be dropped in every place to/through can be used.
I think it's unintuitive because it's taught in that unintuitive way of just saying "zero is first." I know we can't get too terribly deep into this stuff with esp. young children, but we can at least gently introduce them to the concept of offset v. ordinal, as the former is a much more intuitive way to actually
understand array indexes. Trying to liken indexes to ordinal numbers just promotes rote memorization, which is already time which would be better spent on other things.
I've come to feel that "arrays are pointers with offsets" is a largely-irrelevant justification for promoting zero-based indexes in today's programming landscape. Looking at the TIOBE top 10 programming languages for this month, 6 of 10 languages don't even offer a way to address memory with numeric pointers (and that climbs to 8/10 if you rule .NET as "pointers are technically available, but not commonly used and are somewhat discouraged").
If I grab JS/Java/Python/PHP, an array/list is a magic sequence of values with opaque under-the-hood operations, and my only interface to it is the item index. In that context, promoting idioms because they make mechanical sense in a language I'm not using doesn't seem compelling.
I think it’s even harder to wrap one’s head around negative indices in Python, and all the ways to do slicing. They should have added a reversed keyword instead of the current complexity.
My own experience disagrees with your anecdote, but I guess it's different for everyone.
I learned programming by myself when I was a teenager, and 0-indexed never was an issue.
We learn very early on as kids at school that there are 10 digit numbers, not 9. You learn to write 0 before any other characters. On every digit pad, such as phone dial, there is a "0".
If I ask you the list of digit numbers, 0 is the first element.
There is nothing counter-intuitive about that in my opinion, even for a kid.
Interesting. Python is my go to teaching language. It beat the snot out of learning programming with Java. As a newbie, what does "public static void main" mean? So what's your choice for a teaching language? Maybe Lua or Scheme? They seem to have few surprises which is good for teaching.
BASIC, for explaining the fundamentals of imperative programming. Its weakness as a "real" language is its strength as a training language.
Then Scheme, once they feel straightjacketed with BASIC; it will teach them what abstraction is and how to break code into chunks. It should be a bit of a lightbulb moment. They also get a type system.
Finally, Julia, once they get sick of the parentheses and difficulty of building "real" programs (you'll do a lot of DIY FFI to use standard libraries in Scheme). From this they will also learn array programming and the idea of a type hierarchy.
The only trouble with this curriculum is the student will be completely ruined for ever programming in C or Java. They will not have learned to tolerate the requisite level of tedium, or even programming without a REPL.
There's a huge difference between a teaching language for _kids_ and _adults_. That's pretty much the case for most of the skills you would ever teach, not just computer languages.
i have two daughters learning python right now and they did not have an issue with 0 based and i only explained it once.. So i don't think know if i'd say your experience is concensus
0-based indexing with closed intervals is better for slicing. This shouldn't be controversial. It's because you can represent a zero interval cleanly: [3,3) is an empty interval after slot 2, representing a single cell is [3,4).
This has two nice properties. One is that two slices are adjacent if the beginning and ends match, and the other, far more important, is that the length of the slice is end - start.
That's the one that really gets us something. It means you can do relatively complex offset math, without having to think about when you need to add or subtract an additional 1 to get your result.
I use Lua every day, and work with abstract syntax trees. I mess this up all. the. time.
Of course you can use closed intervals and stick with 1-based indexing. But for why you shouldn't, I'm going to Appeal To Authority: read Djikstra, and follow up with these.
> 0-based indexing with closed intervals is better for slicing. This shouldn't be controversial.
base is irrelevant to this, and you want (and show!) half-open, not closed, intervals.
> This has two nice properties. One is that two slices are adjacent if the beginning and ends match, and the other, far more important, is that the length of the slice is end - start.
Yeah, those are all properties of half-open intervals, irrespective of indexing base. It would be as true of π-based indexing as it is of 0-based.
It's related to 0-based indexing in that if you you want to take/iterate over the first `N` elements, `0:N` works with 0-based indexing + close-open, but if you had 1-based and close-open, you'd need the awkward `1:N+1`.
This is why 1-based index languages normally use closed-closed intervals, so that they can use `1:N`.
I'm a die hard Julian (Julia is 1-based), but I do a lot of pointer arithmetic in my packages internally. I've come to prefer 0-based indexing, as it really is more natural there. 0-based plus close-open intervals are also nicer for partitioning an iteration space/tiling loops, thanks to the fact the parent commented pointed out on the end of one iteration being the start of the next. This is a nice pattern for partitioning `N` into roughly block_size-sized blocks:
iters, rem = divrem(N, block_size)
start = 0
for i in [0,iters)
end = start + block_size + i < rem
# operate on [start, end)
start = end
end
But that's only slightly nicer. To translate this into 1-based indexing and closed-closed intervals, you'd just substitute the `# operate` line with
# operate on [start+1, end]
the `[0,iters)` with `[1:iters]`, and `i < rem` with `i <= rem`.
1- vs 0-based indexing is bike-shedding. A simple question we can all have opinions on that's easy to argue about, when it really doesn't matter much.
Julia uses 1-based indexing, but its pointer arithmetic is (obviously) 0-based, because pointer arithmetic != indexing. Adding 0 still adds 0, and adding 1 and `unsafe_load`ing will give me a different value than if I didn't add anything at all. (This is just reemphasizing the final point made by the blog post.)
I have on occasion, and when working on custom array types I've added support for being offset with optionally compile-time known offsets.
As StrideArrays.jl matures and I write more libraries making use of it, I may use 0-based indices more often. The idea of dynamic offsets when they aren't needed bothers me, even though I've benchmarked that as being pretty meaningless.
The bigger problem is just that OffetArrays are a wrapper, and there's a Julia bug where TBAA information on wrappers often gets lost, so that the compiler reloads the pointer you're loading from on every iteration of a loop. Aside from that being slow itself, it also causes the autovectorizer to fail. This causes a severe regression when it occurs. Performance should be fine in other cases.
LoopVectorization or `@simd ivdep` should also avoid the problem.
For the most part, my preference on 1 vs 0 is weak, and for coffee other people are likely to look at i do want to make it easy to understand / not full of surprises.
The natural representation of intervals for doing arithmetic on is 0-based half-open.
Half-open because of the slicing properties, as noted in your posting and the grandparent posting.
0-based because of the simplification for converting between relative coordinate systems. Suppose you have one interval A represented as offsets within a larger interval B, and you'd like to know what A's coordinates are in the global coordinate system that B uses.
This is much easier to compute when everything uses 0-based coordinates.
I think this is a good write up, but I think the notation is still carrying some mental baggage. It's not necessary to have these open and closed brackets/parenthesis. They don't add anything, and if anything, they confuse the matter. An interval is just (1, 2) (or [1, 2] if preferred aesthetically). Since a base cannot be "on" either 1 or 2, it's not meaningful to have these open/closed interval notion. In other words, (1, 2) == [1, 2) == (1, 2] == [1, 2].
Open/closed intervals only come into play in continuous dimensions. DNA sequences, arrays in memory, et al are discrete.
On the first point, yep, completely misspoke, what you don't want is open intervals.
To the second point, as I said: Djikstra's argument for using 0 instead of 1 with half-open intervals is, to my taste, perfect. As I have nothing to add to it, I will simply defer.
For </<= intervals and 1-based indexing you have to write `for(i=1; i++; i <= N)`. So you lost nice property of having `upper - lower` number of iterations.
For half-open intervals, you have upper-lower iterations regardless of base. In C for loop terms, it's:
for (i=lower;i<upper;i++) {}
If you are using 0-based indexing, lower is zero and upper is the number of elements for a full iteration over all indexes; with 1-based indexing lower is 1 and upper is one greater than the number of elements.
Now, despite the utility of half-open intervals, most people’s intuition is around ordinal index ranges, so 0-based indexing is counterintuitive with half-open intervals because slices start at 0, and 1-based indexing is counterintuitive with them because they end at N+1.
This is because, useful or not, half-open intervals are counterintuitive, but they are worth developing the intuition for because they combine nicely.
> with 1-based indexing lower is 1 and upper is one greater than the number of elements.
Do you see +1 tweak? Also consider that "for loop" can't be expressed as </* interval and always expressed as <=/* interval. So 1-based indexing have to be either 1 <= i <= N (closed interval, bad, N - 1 != number of iterations) or 1 <= i < N + 1 (half open interval, good, but +1 tweak).
> I use Lua every day, and work with abstract syntax trees. I mess this up all. the. time.
I think this is the clincher in your argument (rather than Djikstra). I only use languages with 0-based indexing, and it seems natural to me, but I could almost be convinced that if only I used 1-based indexing regularly then I'd be fine with it too. But here you are with actual experience of using 1-based indexing regularly and you don't feel that way, which blows that idea out of the water.
A programmer “strengthens their ankle and leans forward by holding their arm out, preparing to relax the shoulder” into a bar. The bartender bursts out laughing for few sprints.
I mess it up as well, both in Lua and C. The key to success is to not calculate values by adding or subtracting them, but to use meaningful names instead. E.g. dist(a, b), holen(a, b), endidx(start, len) and so on. Forget about ever writing “1” in your code, point your finger and call operations out loud. It doesn’t eradicate errors, but at least elevates them to the problem domain level. Off by one is not exclusive to 1-based or 0-based, it is exclusive to thinking you’re smart enough to correctly shorten math expressions in your head every damn time.
(But sometimes I also feel too clever and after some thinking I’m just writing (i+l2+b+2) because it’s obvious if b already comes negative and means “from behind” in “0-based -1 is last” terms.)
True, but there are examples where 1-based indexing is easier, like returning the last element based on length. I think array[array.length] is easier to understand than array[array.length - 1].
Or the predecessor of the last element: array[array.length - 2] makes you think, whereas array[array.length - 1] is more obvious.
I think the most mathematically natural way to interpret negative indices is to threat them as numbers modulo list length. Then 0 = length, and -1 = length - 1.
Negative indices have an implied ”len(list)” in front of them. They can be seen as an application of the idea of half-open intervals (0 <= i < len(list)), so it makes sense that they’re not symmetrical.
I'm not sure on what principles you'd make a principled justification. My most common use of negative indices is for slicing the end of the list, in which context the interpretation similar to len(list) makes sense. E.g., list[:-2] does what you'd expect (the same as list[2:], except from the other end).
> implicitly taking indices mod len(list)?
Isn't this doing that (plus some bounds checking)? -1 mod 4 evaluates to 3.
Which Lua has also, for strings, and it's quite convenient.
Doesn't work for tables, though, because you can actually put something at foo[-1], which can be useful.
I find that foo[#foo+1] is more common than foo[#foo] in my code, that is to say I add to an array more often than I access the last slot. Although I typically use the idiomatic table.insert(foo, bar) instead— but that's mostly because it's slightly awkward to add the +1 all the time.
Sure. Let's take array elements in reverse, from m to m-n.
Suppose n erroneously becomes larger than m. That should
produce an error, not silently take several tailing elements.
imagine if you used list[index] where index would be calculated
if you misscalculated something and went on negative, then you'd have big exception/error, but when negative indices are viable, then the code will work fine
The fact of the matter is, for a type that can hold N distinct values, there are N + 1 different possible interval lengths. You can not represent intervals over a fixed-width type in that same type.
Did you mean open range? I've never encountered a circumstance where closed ranges are useful, though I presume they exist.
And yes, I think any typed language with a less expressive range syntax than Ada has some work to do. That still leaves open the question of the default, and I maintain that 0 <= .. < n is the correct one.
That... makes no sense? Whether you start counting a list at 0 or 1, if your language of choice supports [x,x) notation, that syntax will effect an empty interval, and [x,x+1) will effect a single cell, no matter whether your list indexes the first element as 0, or 1. The only difference is which cell [x,x+1) refers to, which is the part that keeps sparking the controversy.
Why on Earth would I want a "zero interval" slice?
It has none of the properties I want in a slice, and that a slice of literally any other length will have. In fact, it means every slice I use needs error-handling, because I might get... something that's functionally not a slice, but is still called one. If that doesn't scream "type error" at you, I don't know what would. In fact, that's precisely why 0 is a comparatively recent invention. Most list-based problems were solved just fine before then.
All these arguments are poor rationalisation for the field's inability to move on from the 8-bit era, where the loss of 1/256 of your address space mattered and compilers had bigger problems to solve than translating indices to offsets.
You want a zero-slice because it’s a simpler base case in almost any even mildly complex algorithm. Without empty lists, you need many additional branches throughout a code base to handle the “no element” case, causing more potential bugs. There’s nothing “8-bit” about cleaner code, quite the opposite.
Wikipedia says that Egyptians had a 0 as early as 1770 BC - almost 4000 years ago. If that's a "relatively recent" invention, what about computers?
0 comes up if you're doing any arithmetic at all. It's a natural and useful concept for almost anything. As long as you always can take something aways where there is something, you'll need 0. It wouldn't be very useful to make something such as a non-empty list type, to then be able to take away all elements except the last one, which must remain.
In more mathematical terms, 0 is called a neutral element (relative to the addition operation), and almost anything you might want to do (for example subtraction) requires as a consequence a neutral element.
Well, you might want a function which returns String, not Maybe(String), so that you can just concatenate, rather than handle Some(String) or None all the time.
So that's why you might want an empty String. If you have an optional element in a syntax, it can be convenient to match it with a rule using a Kleene star, and that gives you an empty Node, which would return an empty String if you request its value. And so on.
With open intervals, you have to represent that as, say, (3, 2). Which sucks.
Just a friendly correction :-). You have repeatedly confused the meaning of open and closed. "Closed" means including the end element given in the range, and "open" means excluding it.
It may make more sense to think of intervals in the real numbers. "[1,2)" doesn't have a maximum to the right - it has 1.9999....... but not 2. Thus it's "open" to the right. Whereas "[1,2]" includes its maximum (2), so its sealed.
The terms open and closed apply to any set of numbers (not just intervals), where "closed" means that the set includes the limit of any series of numbers in the set, and open means "not closed".
Much appreciated! The terminology and notation seemed backwards to me, and I basically never use it except when discussing Djikstra on HN ;-)
Visually, it looks like ) has more "room" than ], if that makes sense, and that "closed" would mean that the element defines the 'lid' of the range, while "open" means it defines the final extent.
But you can't argue with nomenclature, and extending the definition to the reals helped make it click for me, so I'm unlikely to screw it up again. Thanks!
You could imagine the curved parentheses as "cutting the corner" of the square brackets - or imagine the curve as just gently kissing the endpoint at a single point, while the flat square solidly includes it. "Open" makes sense because it satisfies the criterion that for any number in the interval, you can find a larger (or smaller) number that is also in it - there's "no end".
In mathematics, both are common. For example, when working with polynomials or polynomial-like things, it is common to label coefficients starting with 0. E.g., a0 + a1 x + a2 x^2 + ...
The subscript on the coefficient then matches the power of x, letting you write the general term as ai x^i, which works out great when using capital sigma notation.
One the other hand, matrix rows and columns usually start from 1, so the elements on the top row are usually a11, a12, a13, ..., and the next row is a21, a22, a23, ..., and so on.
In an attempt to bring some unity to the two sides on this issue in programming, let me offer something that I'm sure everybody will be able to agree on.
Once upon a time I was implementing some mathematical code in C++. It was a mix of things from places where mathematicians would number from 0 and where they would number from 1.
I decided that the code would be clearer if the code looked like the formulas in the math papers they came from, which meant I wanted to use 0-based arrays in some places and 1-based in others.
Solution: I overloaded the () operator so that array(i) was a reference to array[i-1]. Then I could use 0-based or 1-based, depending on whether I was using a formula that came from a 0-based or 1-based area of mathematics.
Julia and Fortran, languages designed to be used for mathematics and high performance scientific computing, take a similar approach in the language itself and support arbitrary starting indices for arrays. Sometimes the math is just cleaner when the index starts at 0, or 1, or 2, or -1!
And of course there is C++ libraries to do the same. For example the blitz++ library. Negative indices to the left of element[0] are great for ghost zones in codes that do domain decomposition over different MPI ranks.
TBH indexing became much less important with every language adopting some sort of "for all" and map/filter/reduce constructs. If you don't care about indexes you don't need to think about them (finally!).
The remaining cases are by definition edge cases and warrant enough attention that bugs caused by 1- vs 0-based indexing doesn't seem to be a big problem in practice.
It's like with goto and structured programming - people stopped using goto for loops and ifs, so the remaining cases where goto is used as last resort aren't much of a problem. People think hard before doing this.
I'd say it's the exact opposite. Iterating over an array is trivial no matter if you use 0- or 1-based indexing. If that is all you do, the type of indexing really doesn't matter.
It is for all the more advanced uses of arrays and indexing where the base starts to actually matter, and those are not covered by foreach.
I disagree, this depends a lot on your problem space. I mainly do scientific programming and so have to do array/vector slicing etc. and indexing definitely plays a big role.
Do you often find a need to slice hardcoded numbers, however?
I find that the only hardcoded number I often need to slice or index is indeed the start, and most languages do offer something for that such as `slice[..i]` to slice from the start.
Well I was replying to the statement that the OP was saying indexing is less important and I find when dealing with slices it definitely is important (as can be seen in your example).
Generally, I agree with what someone else here stated, 1 or 0 based indexing is less important than open or closed interval. The discussion around these two is typically mixed because 1-indexing uses right-open interval while 0-based indexing uses right-closed.
Yes. I did write Lua for a few years and have a hard time remembering seeing any indexing cases. I probably encountered some but they were very few and far between.
Exactly. When I was a beginner I made off-by-1 errors (in Python mind you) all the time. Now that I iterate directly through arrays or use map-reduce patterns I rarely have to handle the index itself.
I was hoping for an interesting analysis of 0 vs 1 based indexing, but instead got a 3 page rant of HN-this and appeal-to-authority-that which adds absolutely nothing of value to the discussion. It feels more like a bottom-of-the-page angry comment to an article than an actual article.
Yeah, this is a bunch of paragraphs complaining that arguments for 0-based aren't good enough for him, but giving no actual arguments FOR 1-based. That's not how you convince anyone. You can't just say that the other guy is wrong without giving any explanation for why you are right.
Plus, the last bit is just "imagining a guy and getting angry at that guy", the absolutely dumbest form of argumentation.
Not sure what you are trying to say here. It is a well known principle in political campaigning that presenting positive policies tends to gain you more votes than negative attacks on the policies of your opponents.
This is probably because this whole debate is pedantic and irrelevant. 0-base wins, at this point it's not about picking a fork in the road, just swimming up stream for the sake of standing out.
LUA is over 23 years old at this point, it made a arbitrary design decision and has had to die on that hill ever since.
I quite disagree, I think it makes two insightful points:
1. In this discussion, people often overlook that these numbers are applied to two different concepts: offsets and numbering.
2. If C had been designed with 0-based pointers (for offset calculation) and 1-based indices (for numbering the array elements) people would probably find this a strength, to have the most appropriate base for each case. It's an interesting what-if idea that I hadn't seen before.
If C had 0-based pointers and 1-based indices, every CPU in existence would have a "subtract 1, multiply, and add" instead of an "multiply and add" instruction and everybody would think this is stupid because "multiply and add" could be used for a lot of things, but "subtract 1, multiply, and add" has only one use.
Fully agree. Got to the end of the post and was looking for the rest. The ranting could have been fine if there was more information and exploration. As it is, it’s unremarkable and not worth reading
My opinion is that languages where you work with vector representations primarily (or at least often) like Fortran, Matlab, Julia are best with a 1 index.
They are languages primarily used for math and simulations and are pretty specialized so the indexing and representation of the vectors matches closer to how you would consider things if you were doing linear algebra by hand.
Julia, Matlab, and Fortran are also all column major for multi dimensional arrays too.
For numeric work ill take 1 index any day of the week, but for general programming or other CS work (like non vector/matrix data structures) i much prefer 0 indexing.
It was super frustrating to see the author dismiss Dykstra’s argument as an appeal to authority. No it isn’t, it just means that we find the arguments he outlined compelling. As someone who switches between C and FORTRAN daily, it’s incredibly clear that zero based indexing is easier.
Here's two arguments arguments in favor of 0-based 'indexing' (or offsets), that aren't just "because that's how it's done":
1. It is faster. Due to how memory works, 1-based languages need to subtract 1 internally every time you access an array[1].
2. It works mathematically better with some of the most common operations on array offsets, like modulo and division into round/ceil/floor. You'll be peppering your code with +1 or -1 around those a lot if you use 1-based indexing.
I am not sure (1) is a very convincing argument. The subtraction of 1 internally is not necessarily gonna be there once the code gets compiled.
I will personally concede with you on (2), but it's still not very convincing considering that Fortran (a 1-index language) is still (arguably) the most popular language for linear algebra.
Personally, I feel "The Index Wars" are the same as the "Text Editor Wars": a matter for personal opinion.
>I am not sure (1) is a very convincing argument. The subtraction of 1 internally is not necessarily gonna be there once the code gets compiled.
Theoretically one can optimize it - though only when one can statically infer the index and the compiler decides to inline the array access function. If you can't do that, the best you can hope for is a fast CPU instruction like LEA or something.
In theory one could make a lot of things fast, in practice they rarely are, and it's always better to avoid problems now than to pray later.
I mentioned it's theoretically possible and outlined the specific conditions for optimizations to happen.
But I also mentioned that in reality various things often prevent compilers (assuming it's a compiled language, and not interpreted, like that LUA implementation) from applying such optimizations:
- you're programming against an API or are compiling an API (i.e. non-static functions).
- you have time-constraints for emitting optimized code, like most JITs - you can't afford any deep analysis that would enable such optimizations, you're mostly pattern-matching for lower hanging fruits.
Since pictures say more than a thousand words, here's the actual disassembly of that lua function I linked earlier, as shipped by my Linux distribution: https://i.imgur.com/DnqVC8E.png
The green stuff is the "fast path". For 0-based indexing you would completely drop the first LEA and replace those last MOV/SHL/LEA with a (faster?) MOV r,r/SHL/ADD r,m - which is what GCC is likely to generate.
So that's Lua (one implementation at least) in practice. No compiler magic making arrays fast here.
LuaJIT (from my current reading) goes the different route of just pretending the first array element doesn't exist, meaning that in memory they have an element '0', but they simply don't use it. This trades some memory for speed.
From lj_tab.c:
** The array size is non-inclusive. E.g. asize=128 creates array slots
** for 0..127, but not for 128. If you need slots 1..128, pass asize=129
** (slot 0 is wasted in this case).
Personally I quite like this approach. A lot better than hoping compilers will magically fix everything and quite hard to argue with, considering LuaJIT's performance. Though it would be a ludicrous design for a lower-level language.
> It works mathematically better with some of the most common operations on array offsets
The problem is that for every formula or calculation best suited for 1-based indexing, there is another which is better implemented using 0-based indexing. Regardless of which indexing mode you are using, peppering your code with +1 or -1 is eventually inevitable. It's why I think the choice is a matter of preference and has little to do with technical reasons.
Overall I still prefer 0-based indexing - a few bytes of RAM is cheap nowadays, when the calculation can be simplified using 1-based indexing, deliberately not using [0] and pretending that the array begins at [1] is often an acceptable option (the trick was originally invented for porting Fortran code...)
Not convinced that is the case. I do not pepper my code with +1 and -1 while using 0-based indexing.
I can think of a lot of cases where I would do it for 1-based indexing, but very few for 0-based indexing. If you want to make the claim that there similarly many, you're going to have to come up with some substantial examples.
The -1 with 0-based indexing occurs very frequently when using the amount of elements in some collection to derive boundaries of the indexes, which will range from 0 up to and including n-1. Grepping for "- 1" in my code folder returns mostly expressions that look like 'length/len/size - 1'.
This can happen, but it is fairly rare, especially if you favour handling ranges as either (start, length) tuples, or as half-open intervals, with the end index being one past the highest index. This representation simplifies many things, many of them unrelated to 0- or 1-based indexing.
Notably, this requires you to use a signed index type, which is absurd.
The alternative is to use the so-called goes-to operator: for (i=N; i --> 0;). But this actually relies on the postincrement semantics, which is a huge wart.
Your (1) only affects compilation time, and even this is assuming a particular implementation of arrays (which is not a given).
Your (2) I don't fully understand. Why would you need to use "modulo and division into round/ceil/floor" on array offsets? How often you do it?
For what it's worth, I've written a _ton_ of code in an obscure language called Omnimark, which is 1-based. Much of that code involved using arrays. I've just grepped through a large Omnimark codebase I helped to write, and no, it is _not_ peppered with "+1" or "-1".
No, it mostly affects execution time. Whether the effect is meaningful, however, depends on various factors.
> [...] and even this is assuming a particular implementation of arrays (which is not a given).
Eventually, all arrays boil down to pointer+offset memory accesses. Well, if your programming language internally implements "arrays" as hashmaps, then you've already lost performance-wise, of course.
> Why would you need to use "modulo and division into round/ceil/floor" on array offsets?
Whenever you're working with 2D/3D data in manually managed memory.
> How often you do it?
When you're writing web services? Probably not so much. When you're doing image manipulation or 3D graphics? All the time.
Citation needed. Yes, the -1 appears in the source code. It might even appear in the intermediate representation. It'll even show up in the assembly for multidimensional arrays (but it'll all get folded into a single `mov` assembly for vectors).
But I dare you to actually demonstrate a performance difference. Compared to a memory access, a `dec` is free.
That's what I was thinking, too, but most of the time (even if the index is not static) the -1 can be eliminated by just modifying the address at which the array is assumed to start. That is,
arr[x]
compiles into an access at address
(arr - 1) + x
Since the address of arr is statically known, arr - 1 is, too, and the address of "one before the first element" will be what's written as the base for the memory access.
> Since the address of arr is statically known [...]
No, that's almost never the case – only when accessing a static array. Typically, that restricts it to static look-up tables and very simple embedded systems.
At some point I would like to write up a story about this team in my University Object-Oriented Programming class who decided to use 1-based 1ndexes in Java while making a 2-D array for a board game.
I don't want to spoil it, but the following picture of a how they stored a 3x3 tic-tac-toe board is a hint:
Modulus (division) is slow and the length should be pow2 and the operation "& (len-1)". If you do %len, you have far greater issues. I have pretty extensive experience writing hashmap/cyclic buffers and the like. If you have auto-grow structs (and you almost always want that), you want pow2 length arrays. e.g.
This is entirely orthogonal to whether you do "prev+1 & len-1" or "(prev & len-1) + 1". (In fact, the latter gives a more natural construction if you fill downward.)
Adding/advancing is indeed easier. What about going backwards (insert before head or remove from tail), i.e when 0, then length. One way to do it is something like that, assume 2 compliment and there is negative shift left available. Not straightforward:
int h = ( -(--head) >>> 31) ^ 1;//if head was equal to 1 (and only 1), h = 1, otherwise zero
head += h * len; //or shift left, if there is log2 len available; still, mul is a fast operation, unlike div
Of course, it can implemented via branching but that would be a major downside for 1-based idx.
Every =single= hash map worth its salt should be power of 2 length backed array. And the hashing function (for the idx of the array) is not the division (that's slow and cannot be parallelized in the cpu) but a simple bitwise AND.
1) It also affects runtime, when addresses need to have a subtraction, and maybe a multiplication, in the offset calculation.
2) You may need this often in certain network or protocol code, for example.
Regarding the mathematical advantage: it depends on the subfield, but I think 1-based numbering is more common in mathematical notation and algorithms (linear algebra being a prominent example).
Regarding performance: some mentioned that the 1 offset can often be optimized away by the compiler. But even when this is not possible, common CPU architectures include instructions that can apply the offset at no extra cost, see https://stackoverflow.com/questions/28032685/is-there-a-perf... , so 1-based indexing is just as fast.
For 2), in Julia, there is "mod1" function which acts as modulo, with 1 being the smallest value instead of zero. It doesn't really make sense mathematically, but is very useful for looping over circular arrays.
Only one reason exists these days: it prevents bugs because people mix up order, infecting, counting, arrays semantics, implementation, pointer arithmetic
> It really shows how conditioned an entire community can be when they find the statement “given a list x, the first item in x is x[1], the second item in x is x[2]” to be unnatural.
It really shows how conditioned the mankind is that they call the "0th" ordinal number by the name "first". We have never moved past the phase where the concept of "zero" was heretic.
Here + offset makes SO much sense. I think we should move to use it in other contexts too. "0 steps from the start of an ordered list" "1 step from the start of an ordered list" etc.
At the moment we are using two different "scales" for measuring things and labeling orders. We could as well use the letters "A", "B", "C" for the latter, and it wouldn't change a thing. Hindsight is 20/20, but it's just a confusing mishap that we are using "numbers" and "numbers + 1" for the two purposes, where the second one could be expressed just as another "measurement", and we could get rid of the confusing "numbers + 1" scale.
This is a left-over from mathematics where indices are distinct from the abelian group of integers.
So the index "1" as used to refer to vector element for example, is different from the number "1" on the number line. That is, one is an element of an index set comprised of ordinal numbers, while the other is an element of the integers.
The confusing part is, that both use the same symbol when in actuality they represent different concepts. The integer "1" is just a number like √2 or -7. The ordinal "1", however, is the representation of the concept "first".
The ordinal "0" would, if used as an index, always yield the empty set as a result (see von Neumann ordinals), which wouldn't be particularly useful in a programming language.
The mapping that selects elements given an index is arbitrary and can be defined as necessary. This allows for constructs like "x[-1]" mapping to the last element of an array. Here, "1" still means the "first" element, but the sign indicates the negative direction, i.e. start from last element.
In short: there's an argument to be made for having two distinct versions. One (1-based indexing) is consistent with mathematics, while the other (0-based indexing) is consistent with low-level low-level data representation and makes no distinction between ordinals and integers.
> they call the "0th" ordinal number by the name "first". We have never moved past the phase where the concept of "zero" was heretic.
Even in maths it's pretty bad, because the Natural numbers still sometimes don't include 0, so that the 0th Natural number is 1, the 1th natural number is 2 etc. (using 1th instead of 1st to avoid the confusing nature of "first" in this discussion).
> It really shows how conditioned the mankind is that they call the "0th" ordinal number by the name "first".
Well, every component of mankind was born. At birth your "first" year of life is the "0th" year. Right now, everyone is living its "n+1"th year of life (n >= 0).
I think the influence is strong, other than natural.
how conditioned the mankind is that they call the "0th" ordinal number by the name "first"
It really isn't. How many elements does a collection have, if it only has a 0th element? Is it 0? What about the size of a collection [0th, 1st, 2nd]? Or [0th .. 100th]? In language, we use 1-based indices for collections because we enumerate the elements based on counting them. A zeroth element doesn't exist, because if we had 0 elements, the collection would be empty.
Indexing of natural collections is based on cardinality, not ordinality.
> Indexing of natural collections is based on cardinality, not ordinality.
You are exactly right. But does it make sense? I'm not sure. "if it only has a 0th element" isn't in my mind, a relevant question in the context of cardinality, since the answer is the same if it only has the "1th" or "2th" or "3th" element. (Sorry, picked up the habit of "misusing" "th" from another post in this thread.) The base case of indexing is "the start of the list" whereas the base case of cardinality is "empty collection". But should you define the concept of indexing by making an equivalence between cardinality and indexing by "cardinality of collection of elements from the start of a list to the current element (inclusive)" is, in my mind, something that doesn't _necessarily_ make sense.
I admit that it's awfully lot about choices and habits, but at least I find "unpacking" the arguments this way interesting and educational.
In general I think it's fair to say that math has lots of crazy and just plain poor choices of conventions and notations that no sane person would ever think of now; it's just very hard to change something so widespread and ingrained, especially given the extremely cryptic and dense notation that leaves no room for evolution without ambiguity (and it's already hugely ambiguous since of course you can't prevent new concepts from being necessary, so syntax gets reused pretty confusingly, and haphazardly at that).
To put it this way: maths is the legacy codebase from hell; but it's what we have.
To be consistent we should redefine a few more things, e.g. when we take the 0th element of a list we have 0 elements, when we take up to the 1st element we have 1 element, etc.
Joking, but I think there's always be a mismatch somewhere.
Disclaimer: I don't care about this argument one way or another. However I found the author missed the point (as perhaps many commenters did?)
"...nowadays, all arguments that say that indexes should be 0-based are actually arguments that offsets are 0-based, indexes are offsets, therefore indexes should be 0-based. That’s a circular argument..."
Yes, that is a circular argument. It's not the one I would use. C made the decision that indexes and pointers are the same thing. It's a logical argument and one that's not circular. If they're separate things, then yes, you end up in a circle.
For languages that are not C-like, you could argue "pick an idiom and stick with it across multiple languages" or "make programming languages easier for humans to understand" Both of those arguments are preferential arguments. Perhaps there could be a study comparing the merits of each, but I haven't seen one yet.
I like chocolate ice cream. I like making pointers and arrays as similar as possible. I like being able to collapse pointer arithmetic down. I like doing huge mallocs and then playing around with large, empty hunks of memory. Some folks don't. I get it. Code should look like the way we think about the problem. That's an ideal state we'll never reach, but it's worthy of continued discussion.
It's missing 'since indexes are 0-based, they're offsets".
Edit: if I would make this argument, I'd say the assumption that indexes are offsets is baseless. This is easily proven since there are languages where this isn't true. However I would argue the problem based on practical merits: the reason there no 1-based index op-code exists is because no dominant programming language has 1-based indexing. Which is a self-fulfilling prophesy.
I didn't want to go there, but yes. If I had called that out, it would lead to a discussion about just what is meant by the term "offset". Defined broadly enough, the point worked for the author.
In general, I try to find the most general point and rebuttal as possible. These discussions tend to dive into pedantry and semantics. I'm just trying to do my part to engage the author where they are. ymmv
Seriously, the exact value of the lower bound for indexing doesn't matter here (some algorithms are best described with the lower bound other than 0 or 1, for example). The fixed or preferred lower bound is the real problem. Any argument for/against 0-based and 1-based indexing tends to gloss over the real problem because those arguments only exist to make some languages look better than other languages. As we move away from forced explicit indexing (e.g. arr.first() or foreach instead of arr[$LBOUND] or `for (i=$LBOUND; ...)`), it becomes clear that there is no such thing like the preferred lower bound for sequences at all.
Fun fact: Hisham, the author of the blog post we're discussing is a notorious user of 3 space indentation. It actually works quite well in Lua, where blocks are terminated by the "end" keyword. Still sacrilegious if you ask me though :)
Just like big- vs. little endian issue, the differences matter less than everyone adopting one. When you program having two code sets with different base of indexing is several times worse than either alone.
0-based indexing is here. 1-based has no benefit. New languages should use 0-based indexing.
Fortran predates C, so 1-based indexing has been “here” arguably longer than 0-based indexing. Most languages targeted at numerical computing use 1-based indexing because mathematics uses 1-based indexing in linear algebra. That seems like a huge benefit to me! C has 0-based indexing precisely because it doesn’t have genuine multi-dimensional arrays and has to use pointer arithmetics instead (which in my opinion makes it a very bad fit for scientific computing)
Mathematicians can, and do, use both, and would arguably be better of with 0-based. Mathematicians sometimes need to use partitioned matrices, and describing the start and end indices of those partitions goes better with 0-based indexing of matrix elements, so that the top left element is at 0,0. It's horrible if you have to work with such things in 1-index based matlab, the amount of nested +1 and -1 corrections you need to type...
0-based indexing was "here" long before C was created.
Lisp or Algol are more or less contemporary to Fortran. The former uses 0-based indexing, the latter can use arbitrary bounds. As does Fortran at least since the seventies, by the way.
I like Ada's indexing range types, if you do it the right way your arrays can be 0-index or 1-index or indexed from 23 to 149. The bottomline is that in a good language it shouldn't matter and the programmer shouldn't even know how the compiler indexes the data. You iterate from array'first to array'last.
I do prefer 0-based indexing, of course, but often using indices can and should be avoided anyway.
This is where I land. Anyone arguing one way or the other tends to make up some arbitrary reason or impose assumptions on the implementation of the language. They are both perfectly fine and logical numbering systems, however since 95% of developers expect a 0-based numbering system, that is what we should use.
This ignores the fact that people use different languages for different purposes. More people use Excel, which is 1-based, than all other languages combined. Should Excel be 0-based because people who use C (a much smaller minority) prefer their language to be 0-based?
Yes! And it's not a coincidence Fortran was conceived to target scientists, while C was targeting kernel developers. The target audience had much to do with the choice.
It seems clearly the motivation was to be in line with other mathematical languages. But then time and again I see people say Julia is supposed to replace Python in the future. Well, you've just set yourself up for a little fight there because I'd say the vast majority of programmers are used to 0-based by default, and they're not going to dick around and install packages and crap to change that default behaviour, least of all when trying to be convinced that some new language is superior.
I kinda take issue with his complaining about referring to Dijkstra's argument as being an argument from authority. Dijkstra makes a pretty solid case in his writing, it's not just about him being Dijkstra.
But we don't need to rely on Dijkstra's opinion at all. Plenty of us have programmed with both 1-based and 0-based languages and (most of us) will tell you that it doesn't really make much of a difference in practice. It (generally) doesn't change how you conceptualize your program or how you express it to the computer. Its like tabs and spaces - pick one, life is too short to care.
Async vs threads matters more. Cargo vs NPM vs pip matters more. Practically speaking programming lua, do/end instead of {} took me longer to get used to than 1-based indexing.
> (most of us) will tell you that it doesn't really make much of a difference in practice.
I disagree, I had to slice bitfields in Ethernet packets in Lua and I hated 1-based indexing very much!
IMHO languages should either have 0-based indexing or any-index (as Ada does) unless you really wants to be a "mathematic only language" which is why I think that it's OK for Julia to be 1-based but not OK for Lua.
> But we don't need to rely on Dijkstra's opinion at all.
I don’t. I refer to Dijkstra’s opinion because it happens to coincide with my opinion, it’s already written down, and it would be silly to spend effort explaining it over and over again.
But Dijkstra's opinion is not well founded, so it's definitely an appeal to authority. His opinion hinges on words like "ugly" and "preferred" without explaining what he means by this. He makes a choice of one over the other forms based on an unclear value function. His entire argument is this:
"There is a smallest natural number. Exclusion of the lower bound —as in b) and d)— forces for a subsequence starting at the smallest natural number the lower bound as mentioned into the realm of the unnatural numbers. That is ugly, so for the lower bound we prefer the ≤ as in a) and c). Consider now the subsequences starting at the smallest natural number: inclusion of the upper bound would then force the latter to be unnatural by the time the sequence has shrunk to the empty one. That is ugly, so for the upper bound we prefer < as in a) and d). We conclude that convention a) is to be preferred."
I mean... that doesn't exactly settle it. Sure, your opinion might coincide with that, but this is not a logical argument. First you're going to have to explain what unnatural numbers are (because they're not a thing as far as I can tell), and then you'll have to tell me what about them makes them "ugly", and how that makes the other option "preferable".
Natural numbers have a least element X. (In fact, any subset of them does. This is called the well-ordering principle.)
Part 2:
Suppose lower bounds are exclusive. Then the lower bound L of any interval containing X must be less than X. Since X is the smallest natural number, L must be an unnatural (non-natural) number.
It is more convenient to stay in the naturals.
Therefore, it is more convenient to use inclusive lower bounds.
Part 3:
Suppose upper bounds are inclusive. Then, to represent an empty interval, the upper bound must be less than the lower bound. (If it were equal to the lower bound, we would get a singleton interval, not an empty interval).
It is already strange that we need an upper bound that is less than the lower bound. But there's more: When the lower bound is X, this would again take us out of naturals.
Therefore, it is more convenient to use exclusive upper bounds.
Conclusion:
It is more convenient to use inclusive lower bounds and exclusive upper bounds, i.e. convention (a).
I know what natural numbers are. But "unnatural" numbers is not a term people use.
"It is more convenient to stay in the naturals."
Right here, you do the same thing Dijkstra does. You declare that one way is more "convenient" than the other (he calls it ugly), without defining what "convenient" means. This makes the argument subjective. What you think is convenient, others may not think so. Or, one may agree with you on a matter of convenience, but would prefer a different choice due to some other priority e.g. learnability.
So I just don't agree this is an airtight argument. It's a subjective argument for a preference at best.
> You declare that one way is more "convenient" than the other (he calls it ugly), without defining what "convenient" means. This makes the argument subjective.
Nope.
1. Some things are objectively more convenient than others.
2. I've explained exactly what I mean. And so did Dijkstra.
Since you apparently don't want to use natural numbers for indices, what do you want to use?
Djikstra does a good job of selling the virtues of 0-based indexing, but he doesn't give the same attention to the situations where 1-based indexing is better. That's the biggest problem I see in EWD831.
For example, if you need to iterate backwards then it is nicer if the interval is closed on both ends (as is the case with 1 based indexing). This way, iterating backwards is as clean as iterating forwards.
While I think these debates healthy (when they're civil and well grounded), found out that I don't have any preferences for one over other for things like array indexing.
Every language has its own design decisions based on some requirement or opinion, and while I have rather strong preferences about languages, these kind of design decisions doesn't strike any nerves.
There are many situations where 1-based indexing is more natural. There are also many situations where 0-based indexing is more natural. And also many (possibly most?) situations, where it doesn't really matter.
My 2 cents: it isn't really that important. As long as the developer is aware of whether the language they are currently using 0 or 1 based indexing.
For a general purpose programming language 1-based indexing probably causes more confusion and errors than benefits. A language trying to be a general purpose computer programming language shouldn't abstract in a way that's likely to lead to confusion with those that understand the fundamentals.
For a domain specific language focused around simulating physical things, or a specific application, 1-based indexing may be the more appropriate option. In this case, abstracting the problem domain is likely more important than adhering to the limitations of hardware.
I don't think it's a real fundamental though - at least not for computing in general. It is true for C, and it is likely true for the many languages that implement things like C. But it's not inherently true that the 'fundamental' implementation of an array must exist as a C array does.
I don't know if it relies on C so much but what C is an abstraction over. An array in C is a set of data that exists in consecutive locations in memory. Memory addresses start at 0. Technically, a C array located at the start of addressable memory, by the nature of the way we've decided computers work in general, would start at 0. Plus as many extra zeros as your architecture can represent. This is generally the way computers are assumed to work.
Higher level general purpose programming languages exist primarily to serve as an abstraction over the fundamental architecture of computers. C is technically a higher level language than machine code and assembly code, both of which use the 'lowest address is zero' abstraction. Every other abstraction, including C exists over top of that.
My point is mostly, if you're trying to remain relatively close to abstracting over general computing keeping arrays and their index based on what they are fundamentally abstracting over makes sense.
In domain specific applications, it may make less sense.
I still find this view of programming languages to be too limited. It's still very C-focused, in the sense that it seems to assume every language is trying to do the same things C is in roughly the same ways C does it.
Yet this just isn't the limit of computers or programming languages. Consider Prolog or a Lisp. What meaningful abstraction do they provide to have a deep connection with a block of contiguous memory used to store multiple same-sized elements? I'm sure you'll find such things ultimately used if you go deep enough, but it's not a meaningful part of the abstraction the languages provide.
And if we can do away with the entire concept of a set of same-sized data stuffed into a contiguous block of memory, what need can there be to adhering to considering indexes as memory offsets?
The incredible amount of (possible) separation between programming languages and the exact way in which such programs are executed provides an extremely valuable and deep freedom in how programmers can think about and address problems and their solutions. I don't see how insisting that we minimize any possible differences between language abstractions and common hardware practice gains us anything.
Though it is a convention, it has demonstrable, objective benefits.
The use of 0 as the basis means that, in mathematical language, the indexing is homogeneous and that conversion between different units is a linear map.
Imagine if we have tape measure that measures both meters and centimeters. Imagine that the first tick on the tape measure, representing no displacement, is simultaneously labeled "1 cm" and "1 m" instead of 0.
Now, we no longer have a linear map to convert between cm and m. What we have is an affine map, like m = 1 + (cm - 1)/100.
An affine map is a strictly inferior alternative when we have the freedom to establish a linear map.
Homogeneous/linear is the superior default. One-based can be used in the special cases where it is nicer.
Here is one example: binary heaps. When we store a binary tree structure into a heap array, 1 based indexing makes the calculations nicer for navigating from parent to children or vice versa:
[1]
[2] [3]
[4][5] [6][7]
The children of every node [n] are [2n] and [2n + 1]. The parent of every node [n] is [n / 2] (floor-truncating division).
Under 0 based, it's not as nice:
[0]
[1] [2]
[3][4] [5][6]
The children are now [2n+1] and [2n+2]. One extra addition is required in the case of the left child. Finding the parent requires a subtraction: [(n-1)/2].
Another way to see the advantage of 1 based here is that every row of the tree starts with a power of : [1] [2] [4] ...
Because we are dealing with exponentiation, avoiding 0 helps: 0 is not a power of two, so to "boostrap" the exponentiation, we have to displace it.
Note that even though it is nice for a binary heap to use 1 based indexing, we still want to store that in an zero-based array, and just sacrifice the storage for the zero element.
If we use a zero based array, then the nice heap arithmetic we wrote in the source code will look nice in the object code, due to the map from the source code array to the object code array being a linear map, rather than an affine map.
It is almost always better to simulate a 1 based array by sacrificing a storage element, than to have the compiler uglify the beautiful indexing calculations for the sake of which we switched to 1 based in the first place.
In summary:
1. we should choose the representation which offers the most succinct indexing calculations for the given situation, and not for some emotional reasons like "children learn to count from 1 and non-programmers understand that best".
2. arrays should be zero-based to preserve the succinctness of the calculation through to the object code (linear map from source to object, not affine).
I don't have a strong position on 0-based versus 1-based, but Dijkstra's argument does not make any sense to me.
He says that expressions A ≤ x ≤ B, A < x ≤ B, and A < x < B are confusing and a source of errors, but A ≤ x < B is not.
I really cannot understand what's the difference, and why one would be more mentally taxing than another.
(From that he concludes that iterating over 0-based array is more intuitive. This kinda/sorta makes sense, if you believe the initial premise).
I'd like someone to shed light on this, if possible.
One interesting case is when partitioning on integers, but wish to compare with floats... How do you get all values in say in 0..9.9999 when using 0 <= x <= 9... it makes sense to use 0 <= x < 10...
Another case is when using datetime... how do you specify the whole day without knowing the precision of your datetime (seconds, milliseconds, nanoseconds...)...
again, specifying 2021-01-10 to 2021-01-11 makes more sense than using 2021-01-10T23:59:59.999 for the upper boundary.
It's just fine for negatives. If you slide the range that 'makes sense' back by 10 (add -10 to it):
-10 <= x < 0
This excludes the end of the range, it includes the start of the range. What's changed is that the start of the range is no longer at zero and also has crossed from positive to negative.
I hope you are not iterating over floating point numbers between 0 and 9.9999 :-)
But your example is just the case for using 0 <= x < 10 in that _particular_ case.
However, if I want all numbers greater than zero and up to and including 10, I'd write 0 < x <= 10.
So I fail to see your point. Do you argue that if we had to choose just one of these expressions, 0 <= x < 10 would have more utility? I don't know how to evaluate this conjecture, but suppose it's right.
Why would you need to limit yourself to just one, though?
Why not to use one that is most suitable to the problem you are dealing with?
His argument works by exclusion; he excludes every notation that doesn't work with just the natural numbers.
Using strict inequality for the lower bound means you can't express sequences starting at 0 without expressing your lower bound as a negative number, which is ugly. So he prefers non-strict inequality for the lower bound.
Using non-strict inequality for the upper bound causes a similar problem. Consider the sequences (non-strict inequality on both sides) [0..2], [0..1], [0..0]. The first has three elements, the second two elements, the third one element. If you want to express a sequence with no elements starting at 0, that is impossible without using a negative number for the upper bound ([0..-1]). So he excludes using non-strict inequality for upper bounds.
What we're left with is non-strict inequality for the lower bound and strict inequality for the upper bound. This way we never have to leave the set of natural numbers to express natural number sequences, and as an added bonus, the difference between our bounds is equal to the length of the sequence.
Note that this part of the argument doesn't touch on 0 vs 1 yet, that is argued later in the essay.
I don’t have any experience working with 1-based indexing, but it seems reasonable. I certainly remember how confusing 0-based indexing felt for a long time when I first learned to program.
On the other hand, I’ve made an observation about timelines in music production software, where bars are counted starting from 1. As you zoom out, only every 4th bar tends to be labeled, which creates this strange sequence: 1, 5, 9, 13, 17… I always found that incredibly hard to reason about, and perhaps it would have been a better idea to label the first bar as 0.
Exactly. There's tons of little annoyances that arise from 1-based indexing. Working on software projects about data analysis or similar makes this crystal clear. 0-based indexing may not be natural for human society, but it very much is natural for calculations and operations on data - exactly what programming languages are meant to do.
0-based indexing is preferable because the mathematics of half-open intervals make composing ranges far simpler. It becomes a bit more obvious as soon as you do any kind of non-integer ranges, because you find that only integers have a reliable "subtract one" operation.
For instance, suppose you're doing a lot of date arithmetic, and you might include times. If I have a range:
[2005-05-05, 2007-07-07)
This unambiguously includes all times. If I compose it:
[2005-05-05, 2007-07-07) [2007-07-07, 2008-08-08)
If I shift all of them forward 10 days:
[2005-05-15, 2007-07-17) [2007-07-17, 2008-08-18)
Every possible time within the new range is accounted for. Transformations on the ranges are obviously correct. If we can't agree on whether time resolves to seconds, milliseconds or nanoseconds, it doesn't matter.
That's because you can find the beginning and end of a range without knowing how to subtract by one. If you later decide to add times, it will just work because, conceptually, you already handle every possible value in the range.
If you want to construct a series of ranges of floats, it's a bad idea to do [3.457, 6.799999], [6.8, 12.47699999]. If you use half-open intervals, it just works, without mucking with the end of the range.
This is the reason that "indices are offsets" is powerful: it's not circular reasoning, but a simplification because we're removing an unnecessary primitive (subtracct 1) that only reliably works with integers.
And to bring this back to integer offsets, even when you're working strictly with integers, you may want to conceptually work with rational numbers.
When you're working out your math for buffered IO, you need to map byte indices to chunk indices. You need to assign every byte written to a chunk.
In a 0-based world, your chunk offset is simply your byte offset / chunk length. Because there is no distinction between indices and offsets, it's a clean mapping between one offset scheme and the other.
It means you can work out on paper when a new chunk should be started, and then translate that into integer arithmetic. And your code directly reflects the math, rather than having to stick "- 1" and "+ 1" in here and there.
> It really shows how conditioned an entire community can be when they find the statement “given a list x, the first item in x is x[1], the second item in x is x[2]” to be unnatural.
It's not unnatural, but it's not natural either. It's just that 1-based indexing aligns with natural language conventions better. It also matches with some math conventions for matrices and vectors, but those don't have to use 1-based indexing either to work. I learned linear algebra with abstract "index sets" (I), where I can be {1, 2, 3, .., n}, {x,y,z}, {0,1,2}, whatever.
Programming in 0-based indexing means way less +-1 adjustments on boundaries. Division/modulus also works way better with 0-based indexing, so flattening multi-dimensional arrays is more easily expressed too.
In the end it's just a convention, it shouldn't matter too much. My money is still on 0-based indexing, as it lends itself to less errors and cognitive load when you get used to it. I used both extensively.
Tangential: conventions that are more "natural" are usually not actually natural, but just align themselves to other conventions better. Of course it's always good if conventions align, but sometimes it's inconsequential. Also there are situations where we can't make all conventions align.
An other little pet-peeve of mine is people calling big-endian more natural than little-endian, where there are actually different competing conventions:
1. Usual way of printing memory content from lower address to higher address from left to right (think hexdump).
2. Usual way of writing down numbers from left to right from most significant digit to least significant digit.
3. Expressing the value of the number from the byte array elements (\sum_0^(width-1) byte[n] 256^n for little-endian, \sum_0^(width-1) byte[n] 256^(width-n-1) for big-endian)
Big-endian aligns better with the combination of 1 and 2. Little-endian aligns better with 3.
Well, that's usually a more complicated discussion. In French and Romanian at least, the word used for "floor" in this context ("etage"/"etaj") refers to an elevated construction. So, a building with "1 floor" ("1 etage") has 1 elevated construction atop the ground. This is what leads to the apparent 0-based numbering. The numbering is actually 1-based, but people count the extra layers above the ground, instead of literally counting the floors. In fact, I have rarely seen elevators show a 0 - they normally have something like P, 1, 2, 3.
It's more complicated anyway, since there are many structures with intermediate floors below or slightly below the ground, and those are usually nominated specifically ("mezzanine", "demisol" , etc.).
This is not the best example. In Polish for example there is a whole different word for ‘0’ floor (parter), and the word for the rest of floors (piętro) kind of assumes that level is above ground, so we do use 1 based indexing there.
I think a stopwatch is a different beast from 0-based or 1-based indexing. A stopwatch is used to measure the time interval between two events. If stopwatches could somehow magically measure and display time in infinite precision we wouldn't have this discussion. But if they do rounding to the nearest displayable number then they display 0 for half of the time-step interval, then display the subsequent numbers for the full time-step interval each.
I vaguely remember that the original minesweeper started counting time from 1, which is certainly odd. I could be wrong here though.
One issue I see with 1-based indexing is that it makes x[0] undefined, so even if you pass an unsigned integer index to a function you need to verify that it's != 0 before you use it. So in a way it'd be the whole null pointer thing for array indices all over again.
Another minor issue is that you can access one less element with a fixed size integer than when you do 0-based indexing. So for u8 you can only access 255 elements instead of 256 for instance.
Actually, in Lua, which caused the original discussion, it’s legal to use x[0], since the language doesn’t distinguish between arrays and dictionaries (Lua calls them tables). But if you do that, the length calculation will be off by one.
I have seen some difficult to debug issues caused by this when I used to work on game scripts.
That sounds like an implementation bug in the name of speed or memory size rather than a programming correctness bug. What's the correct behavior if the 'table' doesn't have any integers as keys, but instead other elements such as strings?
You can use strings as keys. In fact, that’s how you can create objects. Also negative integers can be used as keys. But array operations will malfunction if you attempt to apply them to such a table.
The tricky thing is that the length operator in Lua (#) actually returns the largest integer index in the table. If you use 1-based arrays, that also happens to be the number of elements in the table. However, if you use 0-based arrays then it won't count the zero-th element.
This is an argument about checked indexing, not about where indexing should start; after all, whatever the index base, you still need to check that the index fits within the bounds of the array. It doesn’t matter if a behaviour is technically deterministic and defined if it’s not what you want to happen. I mean, just read the explanation of the ‘wat’ talk: <https://stackoverflow.com/a/9033306/3840170>. Every one of these behaviours is defined by the ECMAScript standard, but they’re still useless.
Yes, but checked indexing requires only one comparison with the upper limit when your indices are 0-based, instead of 2 comparisons, when the starting index is different from 0.
The sign of the index can be determined without a comparison, usually simultaneously with one of the previous operations.
Another language trivia: for strings, Pascal (which has 1-based indexing) does actually use the character at index 0 to store the string length behind the scenes. Because of this, indices are converted into offsets directly, without having to subtract one. It’s also the reason why strings were limited to 255 characters.
If Pascal had 1-based indexing, it wouldn't be possible to store the string length at index 0. The first character in a string is always at index 1 because of this. Strings are
This is obviously spaces vs tabs third rail in PL design, but riffing off of the language in the article, probably we should all agree on two opinions:
0-based indexing is inherently better for machines.
1-based indexing is inherently more natural for humans.
**
For a short stint I was running a (small) datacenter, and despite being an apologist for 1-indexed programming languages, I zero indexed EVERYTHING in the datacenter.
Let me just say, that was a huge mistake. Physical items are not offsets. Even though I was then working in a 0-indexed PL, The amount of contortion I had to do to remember zero indexing made the likelihood of errors higher (I did not make any that weren't quickly recoverable, but still).
My culture uses 0-based ages, where in your first year on earth you are 0 years old and turn 1 after you have been alive for an entire year, but I am not sure age would be any more confusing if it were 1-based. I don't see anything fundamental there. Other cultures do use 1-based ages (you are 1 on the day you are born) and they seem to get along just fine. Whatever you are accustomed to is what seems natural.
I have experience with 1-based indexes: basic/pascal. My 2nd language after basic was assembler. Off-by-1 bugs were plenty and confusing - mixing assembly with basic and pascal. Zero based with Java/Javascipt has felt a lot more natural than one based. Likely in 20y+ years there has not been an-off-by-1 bug. With that said: 0 based has felt totally fine and natural. All it takes to remember: inclusive/exclusive.
I know Visual Basic gets a bad rap, but as a learning language it had an interesting feature, `Option Base`. By putting `Option Base` in a module, it changed how the indexing of arrays worked. It defaulted to 0, but for some applications (and also, when you're first learning), 1 can be convenient.
Of course, there are problems with this in a professional setting, such as how do you enforce uniformity across modules, and what happens if you copy code from one module that's Base 0 to one that's Base 1 and vice versa. But when I was first learning how to program, it was helpful to me to have a language that allowed for some choice.
In the meantime, 23 years of programming have led me to believe that index base 0 makes sense. For many applications it's moot, because we should be using higher level functions (like map and reduce) for processing lists. In every other application (such as working with data on a grid), dealing with offsets does make things easier.
Perhaps the convention is arbitrary. But, lots of industries have arbitrary conventions that we all agree on just to aid communication, and I disagree with the original author that the term "groupthink" applies in this situation.
While 1-based index is probably easier to learn and understand for a beginner, even as a beginner there is quickly an overhead where you have to adjust indices by 1 all the time as soon as you do any math on indices. Going from Basic to C was hard for me for many reasons, but a lot of complexity in my code that I took for granted just vanished. This truly showed me the power in getting such fundamental choices right.
This type of bikeshedding behavior is a sign of technical immaturity. The more technical experience one gains, one usually becomes a lot more disinterested about small syntactic technical details of programming languages and more interested about larger abstract concepts (e.g. discussions on Monads, type theory, "everything is an expr" etc...)
Zero- versus one-based indexing actually matters somewhat for correctness. The real trivial ‘small syntactic technical’ bikeshedding is braces versus indentation (and: which kind of indentation) or semicolons versus no semicolons.
Semicolons vs. end of line (like in Python) has a significant difference on what kinds of things you can express. End of line based syntax strongly discorages long lines, what brings problems and benefits. (And the Javascript choice is simply wrong.)
Semicolons vs. indentation structure (like in Haskell) has a huge usability difference in interactive shell. It's trivial on editor code.
The dismissal of syntax as trivial is not a good line of thinking. Syntax tends to have surprising impacts on semantics.
yeye, "HN" tends to abstract everything to the point that sometimes I do wonder how Java is not the church language of HN, also I do wonder whether people here still write code, or just became "architects".
So, there's nothing wrong with challenging common norms - remember NULL? XML oriented programming?
Rebutting the dismissal of offsets as an artefact of an old language:
- pointer offsets - the article seems to dismiss these, but they're used loads in systems/network/driver programming, which is a reasonable chunk of the most important programming
- conceptual offsets - e.g. time of day starts at zero, not one. We think in this way a lot, and it's useful.
For things that wrap, such as ring buffer indices or packet sequence numbers in a fixed-size field or clocks (hour of the day, minutes in an hour), zero-based indexing is simple modular arithmetic. Wrapping of one-based indices can of course be done, but it's more complicated than necessary.
Even worse is sometimes this 0-based numbering is carried over to a world of enumerable things, cargo cult like. In my project we have multiple physical entities which have numbers, e.g. blades, ports, channels on so on, all of them are counted from 0 because some genius decided to write that into standard. And now there are multiple points in the code where 1-based numbers are converted into 0-based and back, and incredibly - there are bugs there :) . Talking to humans gets equally weird and confusing when you are talking about second port numbered 1, it's just never gets natural, even after years working on these devices.
Here’s some fuel to the flames: < is less chars than <= , it also is more logical. 0-based indexes have the advantage of if in comparison and range. Mathematics treats 0 as special and can have all sorts of side effects if dropped into a formula, code doesn’t have this side effect (unless running formula as an algorithm, which is math).
To save yourself from headaches, I believe 0-based indexes are preferred in almost every modern language for the simple reason of optimization. I have no evidence to back this. NULL=Nothing=0 is a thing though.
Honestly 0-based indexing is one of the most annoying things to teach. In sciences, people really don't care about programming arcana: I have never found anyone who found 0-based indexing natural or intuitive.
In fact, much of the vehemence in favor of it seem to be more about programmer shibboleths, to make some people feel they are in a special 'in-the-know' club (certainly if the r/programmerhumour reddit is anything to go by).
All I can do for scientists is tell them the advantage of learning a programming language outweighs the weirdness.
just... no. We are not talking about a number line here, we are talking about array indexing. That is a list of things that you count starting from the beginning. Like, no one lines up their children and says 'here is my Zeroth child'.
There is a reason that Fortran, which was designed for science and engineering, uses 1 based indexing: because it makes sense when counting things, as opposed to dealing with some internal arcana of a computer's memory model.
0 based array (python) :
screen_pos = x + y * width
1 based (lua) :
screen_pos = 1 + x + y * width
shrug
So 0 based seems to work a little better for arithmetic based array references. But really you'll never resolve this to perfection. Assembly language programmers are very unlikely appreciative of 1-based.
Its all about what you're comfortable with.
Those wanting pointers can imagine a base address added to each calculation. 1-based then needs a -1... which is then a strange thing to advocate for.
The problem is within the phrasing and its premise: Are you using an index or an offset? 99% of the time I think of an offset, so "first" makes sense as "0". If you go by indexing "1" suddenly makes sense as an index. When people say index in the context of arrays or data structures usually they mean an offset. To me it's simply wrong phrasing.
I don’t know why 1-based-indexing supporters insist on pointing out this distinction. Whenever one indexes some kind of a map with numbers – and it actually matters that they are numbers that you can perform arithmetic on, not just opaque symbols – chances are they will be computed from an ‘offset’ somewhere or vice versa, which means the notions are going to blend a lot.
> You see, Lua uses 1-based indexing, and lots of programmers claimed this is unnatural because “every other language out there” uses 0-based indexing.
> I’ll brush aside quickly the fact that this is not true — 1-based indexing has a long history, all the way from Fortran, COBOL, Pascal, Ada, Smalltalk, etc. — and I’ll grant that the vast majority of popular languages in the industry nowadays are 0-based. So, let’s avoid the popularity contest and address the claim that 0-based indexing is “inherently better”, or worse, “more natural”.
Maybe I missed the comments about 1-indexing being "unnatural" but if the author is referring to the discussion I took part in, then this is a strawman. It wasn't about what was "natural" or "unnatural" but about people switching from their primary language to secondary languages and gotchas like 1-indexing being error prone.
We had a platform team at my last company looking to adopt Lua for client customization. The primary authors became familiar with Lua in writing the code logic but everyone else would be touching their part, or contributing back to the core, not as people familiar with Lua but as C++ developers who would be blindly making changes in another language. It felt similar to maintenance of our Perl scripts. You don't brush up and become an expert on a language you interact with on a yearly cadence. It is important in these cases to have few gotchas to make casual contributions easier and safer.
This says nothing about using 1-indexing when your target audience isn't 0-indexed programmers (speaking of those who choose to use Lua and not to Lua's creators).
Unfortunately, for me, even with my complaints (this and language compatibility), I'll probably still use Lua for some projects of mine. I've looked at others and I'm mainly concerned about the community size.
Honestly, I feel like `some_list.first` is the most ergonomic.
And if the API provides `second`, `third`, `antepenultimate`, `penultimate`, and `last` methods it covers a large part of what one most often use.
Past third rank, then I will feel better served with a `slice` method, whose documentation provides explicitly it’s index convention, whether it makes a modulo on out of bounds, etc.
If you use 1-based indexing, you can't iterate over a list of maximum length with a simple loop. The normal loop condition is `while i < len` (0 based) or `while i <= len` (1 based). If len is maximal, the second one probably has an overflow bug in the loop body that turns it into an infinite loop or a crash.
This is actually symmetrical. When you use 0-based indexing, you can't count down with a simple loop: you need to either extend your index type to include an invalid -1 or apply the infamous evaluate-then-decrement semantic. In practice, this means people often index with a signed integer, which is absurd.
And lest you think that counting down is unnatural—there are quite a few advantages in comparing to zero on every iteration instead of a computed upper bound, like that you can often roll the condition check in with the decrement instead of having to do a separate comparison.
A list of length equal to whatever maxint is is something you would have to set up on purpose. (If it were naturally occurring and unpredictable, then you would need to handle the much more common case where the length of the list is greater than maxint, too.)
The fact that a weird situation you set up intentionally needs special-purpose handling doesn't seem odd or unreasonable to me.
In the same way that I don’t think learning sklearn is the hard part of knowing ML, I don’t think 0 or 1 -based arrays is the hard part of any particular language. There are situations where both are appropriate. The choice of either as the deal breaker for a language is extremely superficial.
I had a ROFL moment when he discredited appeals to authority, and then progressed to note that it's conventional math notation - which is pretty much the same kind of logic.
Furthermore, mathematical notation is almost universally terrible. It's inconsistent, ambiguous, and has many undeclared dialects. Oh, and many people use 0-based index to boot, because, you know, whether that's conventional or convenient (largely orthogonal qualities alas in math) depends on the context.
I mean, I can sort of buy his argument that it's arbitrary, but then he also points out that there's at least one advantage to 0-based indexing, namely that works well in offset based scenarios (not just for pointers). So in which case is 1-based indexing convenient? Based on this blog post, never.
The harder it is to learn, the more it takes you to change your mind. It's hard to see things without bringing baggage
Also, the irony of complaining about HN holy wars becoming just another trigger to continue!
I'll do my part - whichever side you are on - you are wrong.
An interesting perspective is what this does to pointers in terms of unsigned representation, effectively making 0 a special case which could be used for null. But in practical terms it would just limit 32 bit systems to 31 bits of adressable memory, or make pointer arithmetics wierd. The latter isnt that much of a problem, as its only when pointers are basic types it matters, and free pointer arithmetics is a mistake whose only advantage is compiler speed, not even application speed.
For instance, even in slicing, [start:end) eliminates this issue. Though there is something to be said for verbosity.
I think this becomes an argument for 0 based indexing, but a case could be made to the opposite.
That said, consistency is what is valuable, so the only truly wrong answer would be the one we use in real life.
I don't understand why the author treats referring to Dijkstra's paper as "appealing to authority". To me it is natural to defer to a well-written paper instead of repeating the same arguments in your own words, regardless on who wrote it.
We can't even agree on this for floors of buildings. Half the world starts at 'G' then 1, 2, 3 etc. as you go upwards, the other half starts at 1, then 2, 3 etc.
And then you have Japan which sometimes starts at 2F. Hey, maybe we should use 2-based indexing.
I grew up on languages that were 1 indexed - BASIC, VB, VB.Net. It took me a long time to get used to 0 indexed, primarily in college and later in working with more C inspired languages, yet I think 0 indexed is superior. Why?
Numbers in programming start at 0, even in the languages with 1 indexing. Period. When I initialize an integer, unless I explicitly set a value, it’s 0.
From a purely pragmatic standpoint, making the first number an invalid location in a list just adds unnecessary complexity and increases the odds of off-by-one errors. On the same token, there’s nearly no benefit to starting at one.
No, no "period". There are plenty of languages that either require you to initialize your variables, don't allow to use a variable until it's initialized, or give an uninitialized variable a special value like "undef" or "Nil".
I agree that BASIC is inconsistent, but there are many languages beyond Basic and its descendants...
I'm afraid your argument still sounds like "C does it, so it is right" mentality.
The biggest thing about index 0 or 1 is the inclusive and exclusive slicing. When the index is 0 and the slicing is [inclusive:exclusive], I find it a bit easier to write code with array manipulation.
I am pretty sure that the last, counterfactual, paragraph of this article is spot-on.
Personally, I am not so much interested in which way is more right, but which way is less likely to result in off-by-one and related errors, though I do not know how you could gather data on that, other for people beginning programming, and any difference might go away or even flip as programmers gain experience and move on to more difficult problems. (Of course, the most effective solution is to not use indexing at all, except where absolutely necessary.)
Citrine is also 1-indexed (https://citrine-lang.org/#lists), I think this would help people who are not developers to read and possibly verify code a bit easier. Of course it is just a small step, but I feel code in general is drifting away from the normal users, who seems to be confine to graphical interfaces. Wasn't it an objective once in the software development community to try and make code as accessible as possible?
"... of course, nowadays the number one reason is tradition and familiarity given other popular languages, and I think even proponents of 0-based indexing would agree, in spite of the fact that most of them wouldn’t even notice that they don’t call it a number zero reason."
That's it, I'm switching to saying number 0 reason from now on. If you are talking about your number 1 reason, I will forever be wondering what your number 0 reason would be. I'll tell my wife tonight she is my number 0!
Maybe the confusion comes from the fact that programming languages don't make a distinction between cardinal and ordinal numbers.
In English we do. For example, if you are nine years old, you are in your tenth year. If an array has ten elements, the last element is the tenth element, it is nine elements away from the beginning. We could do "int array[10]; array[10th] = x;" if we wanted to mirror that.
We could imagine a language that does the distinction, but I don't know what good it would do.
Wacław Sierpiński (1882–1969), the namesake of Sierpiński's triangle, was famously a proponent of zero-based indexing, including in everyday life. Sadly, I can only find sources in Polish that corroborate this [0].
As weird as I find 1 based indexing(looking at you R) I still get confused by 0 based indexing. This happens all the time when I’m working in pandas and thinking about line numbers in the file vs the data frame, and I recently had an issue with this while implementing Conway’s Game of Life in Racket, while I was traversing the array and when I was looking for neighbors.
It has been enough of an issue for me to seriously reconsider my stance on indexing
After some experience with Go I realized that in quite a few cases it will be nice to have 1-based indexes. If one have a variable that is an index into an array, then it is natural to initialize it to an invalid index like -1. But in Go everything is initialized by default to 0. So to get -1 one needs extra code.
With one-based indexes zero would be a very natural invalid value as an index similar to a null pointer.
My own opinion is that zero based indexing is generally much more useful, and makes a lot of things things simpler and/or more sensible mathematically or otherwise. However, there are some cases where an index based on some other number (often, but not necessarily, 1) is more useful. (Some programming languages, such as BASIC, allow you to define whatever starting index that you want to do.)
It does not seem like a big deal, both conventions are easy to understand and natural to use. Except if you work with the discrete Fourier transform. Then 1-based indexing is extremely cumbersome and annoying. More generally, if your indices are to be understood "modulo N", it makes sense that they are all strictly smaller than N (or even signed, and centered around 0).
Unfortunately off-by-one errors can be insidious and non-obvious to debug, having different systems only complicates this. 0-indexing makes sense for systems and computer programming while 1-indexing makes translating math more straightforward. Inter-converting between the two systems is unfortunately non-trivial, even if the cost isn't that high.
I think it just comes down to where you want your index-out-of-range errors to occur.
Pretty much any language I can think of inits primitives with all bits set to 0. So either, you accidentally forget to set your index vars to 1 and out-of-range it there, or accidentally forget to take away 1 when accessing the end of the array. Pick your poison
I don't think it simplifies quite that far. There are languages that are capable of catching such errors ahead of time. Now that I think about it, there are even more that catch the initialization error - or at least do so more automatically than the end point error.
Perl initializes its scalars to "undef", not 0.
If you try to use undef as an array index, it will convert it to 0 but issue a warning.
I'd prefer it to be an error instead.
The APL language has this system variable ⎕IO (called QUAD IO -- index origin), which could be set to 0 (for 0-based indexing) or 1 globally in the environment.
Also, ⎕IO can be set as a local variable of a function to limit the scope of the indexing origin, which made it convenient to simplify the code to implement certain algorithms.
The funny thing is that even in natural language we don't use base 1 or 0 consistently.
For example why is the 1st of January the first day of the month/year, but your first birthday is when you turn one year old, i.e. when your first year of life finishes.
I'd say from a mathematical perspective, 0-based indexing makes more sense, simply because the Peano axioms start with 0 for the natural numbers. Also, thinking of overflows makes more sense this way, because it's just modular arithmetic.
Discussing about 0 vs 1 indexing is like questioning the key layout of a piano. Sure, one is probably easier to learn than the other but in the end its an interface everyone is used to.
The crazy part is that Lua accepts "array[0]" just fine and it works exactly as expected. This is a non-issue in Lua but many people cite it as a problem.
I never thought I'd say this, but you've got to give props to VB where you can declare the starting (and ending) index of your array to be whatever you want!
This is something that's extremely annoying to deal with in a genomics context. Various formats in the field uses 0-based or 1-based and inclusive or exclusive.
While I don't have a strong preference in this "war", I can think of an interesting argument for 1-based.
Our time counting is 1-based. There is no zeroth second, zeroth minute, hour, day, month, year, or century.
This actually makes it slightly harder in 0-based languages to work with time and dates.
Also, it might be an indication that 1-based system is somewhat more intuitive to non-programmers.
My clocks all start at 00:00:00 every day, and that seems entirely logical to me. It's also how all programming languages that I've worked with represent time.
But 00:00:00 does not represent the first second of your day. It represents a _point_in time, which has no duration.
The first second of your day is an interval between the moment your clock shows 00:00:00 and the moment it shows 00:00:01.
If 00:00:00 is the fist second, why it's not the first minute and not the first hour? Is the fist second is the same as the first minute and the first second?
Our counting is 0-based as well - usually when you start by saying "one" it is implied that 1 second has elapsed. So while we don't pronounce zero we still count starting from it.
Yes, this isn't usually an argument I care for, I happily use zero-based and occasionally having to pause to remember to get it right in SQL is a minor annoyance and that's that.
But then recently I was doing some 'munging' to get some data structured correctly to display in a chart over months, with a variable start month that depended on the data. Well, let's just say that involved a fair amount of +/- 1 ing, and is hopefully the last time I think 'hm, it'd actually be quite nice if you could choose which to use on access' (or per function?).
There absolutely is a zeroth second! It is the 60th second of the prior minute. This is actually the exact reason why midnight is 12 AM not 12 PM, and noon 12 PM not 12 AM.
If there weren't a zeroth second, minute, hour, day, month, year, or century when was 12:00 AM of Jan 1, 2000?
This is why a zero _offset_ is the initial _value_ for many implementations.
... And I (a non-native English speaker living in the US) always have trouble remembering what 12 AM and 12 PM means, even after all these years I have to look it up every time, and sometimes it drives me nuts.
(For a moment I hoped your explanation would at least serve as a heuristics... but alas, it does not make sense to me. Well, at least at the moment I remember the notation).
Regardless of it, "12:00 AM of Jan 1, 2000" is not a particular second. It's a point in time, it has no duration.
It does not belong to any particular calendar hour or day or year, it's a _boundary_ between two.
Of course it belongs to XX century since XXI century starts at 12:00 AM of Jan 1, 2001.
The first second of Jan 1 lasts from 12:00::00 AM to 12:00:01 AM.
First minute of that day lasts from 12:00 AM to 12:01 AM.
Etc.
I'm not French, but your comment just made me 'realise' that people who use 12h have the largest numbers when we on 24h have the smallest, almost like the 'start' is one o'clock. Weird.
Same :00 start happens with minutes on 12h though of course.
I think GP didn't mean to talk about time, has a point regarding months as I replied elsewhere. We represent the months numerically as 1-12, and I have recently found that annoying to work with.
12:00 should actually be 0:00 on the 12 hour clock, it belongs to the next 12 hour period. 11:59 PM is yesterday, 12:00 AM is one minute later, and falls on today. Same with noon, 12:00 PM comes one minute after 11:59 AM. When I see 12:00, I automatically convert that to 0:00, otherwise AM/PM doesn't make sense.
Indeed, months are 1 based, but it's never been a problem to eg compute which month it will be, say 9 months later: just sum and subtract 12, no need for a real modulo computation. It's probable I've never found it annoying because I don't need to perform this computation with more than 12 months.
Yeah, this is most familiar to me too. Still, I'd argue it's 1-based.
00:00 is a point in time, but the first second is between 00:00:00 and 00:00:01, and it's "00:00:01" when it has elapsed.
Similar to the indexing question, this is only true for some cultures:
"East Asian age reckoning originated in China and continues in limited use there along with Japan and Vietnam, and is still common in Korea. People are born at the age of one..."
No it doesn't. That just gives you the last element, which is not a "based indexing".
JavaScript will allow arr[-1] as an "individual" element, but it's internally an object and so iterating and hoping that -1 comes in order doesn't work.
Sure it does. -2 gives second to last, -3 gives third to last, etc. If you only use negative indices i, the "base" of the array is the end, and arr[i] is just arr[len(arr)+i].
First, it's a "joke", second, nobody is going to force you to use non-negative indices. Sure, you can use a base index of zero and read left to right... but you can use a base index of -1 and read right to left and live a full (if lonely) life without considering the passé alternatives.
More precisely, I'm defining the "base" to be the right hand side of the array and operating on the reversed list -- it's just a change of variables. Python supports -1 based indexing, but does not support 1 based indexing. You could easily define a language which only provides the -1 based indexing but does not support 0-based. Let me demonstrate:
i don't really care if the language has me start counting indices at 0 or 1. it's never been the part of the problem of programming that has caused me significant trouble.
You still have to understand what is being abstracted if you want to do anything useful. You cannot just ignore what data structures are and how they work or you will have some very bad surprises.
Also often abstractions (and their quirkiness) make way more sense when you understand what is being abstracted.
If the language doesn't expose pointers, then that's an abstraction leak. Even if it does, why must that dictate the syntax and semantics? I'm sure our compilers and interpreters are capable of turning
Why would you need to "wrap around" an array?
I guess it's helpful if you use an array to represent a cyclic group, but how often this happens in practice?
And when you need the number of the last element, you need to do A[n-1], which is "less helpful", I guess.
I think both those cases happen with a certain frequency, yes, your last element is a bit trickier, though in Python it is represented by A[-1] (so that would turn into A[len-1])
Yeah, maybe there's no fundamental advantage, one would have to check the most common cases.
I find zero-based indexing more natural. Consider the case of dates. Dates are naturally one-indexed. There is no year zero.
This means that the 21st century started in the year 2001. Likewise, the current decade started under a month ago, not over a year ago. If you think the 21st century started in the year 2000, as do most people, then that implies that the first century either started in the year 1 B.C. (ugly) or was only 99 years long (not a century). People naturally (and incorrectly) default to zero-based indexing in this case!
I would go farther and make centuries zero-indexed too. It's far more natural to say that the Nth century is everything in the form Nxx than to say it's (N-1)xx. Every time I run across a numbered century I have to stop and think for a split second to calculate what years it covers. I got this wrong on several occasions in grade school and was marked down for it.
Zero-based indexing works naturally for grouping things. One-based indexing does not.