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.
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.)
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?
Americans do this a lot too, except at a (pseudo) decade level, e.g. "are you a '90s kid?"
This brings up other ambiguities. A "90's kid" might have been born in the 80's.
You could say that the American school year is 0-based. Except instead of naming it zeroth grade, it is named kindergarten.
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.
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
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.
/9..10\ : "short" interval : size 1 (hour) == half-open interval [9..10)
-|20..21|- : "long" interval : size 2 (days) == closed interval [20..21]Ultimately, it relies on conventions, but I guess now you can see how this one relies on something which has some consistent pattern.
those are the same number.
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.
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.
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.
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.
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.
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.
It just makes a point opposite to the author's intent.
Thank you for pointing out my error.
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.
Of course, following a recipe in specific ideal circumstances works, as long as nothing changes.
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.
The kind of understanding you're rooting for relies on formal logic, not electronics.
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.
I think we are on the track to create a perfect programmers factory.
Once someone understands (2), pointers are incredibly simple and obvious.
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.
If the OP was teaching people a language like Prolog then there wouldn't have been the same confusion.
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.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.
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...
Of course, my N is small.
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
x = f0()
y = f1(x)
or x = f1(f0())you can just show them a ruler and ask them where does the first centimeter starts
The first centimeter starts at 0.
The first array element starts at 0.
(Also please note that both this and my previous reply are tongue-in-cheek).
"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.
> 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.
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.
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.
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.)
I certainly had unusually able teachers.
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.
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.
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.
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.
Just like DRM, make it a little harder to skate on by and you actually filter a lot of casual/non-serious people.
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.
All of them.
Also trampolines. Kids love trampolines. From there it's a simple jump (pun intended) to something like the Y-combinator!
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.
Seriously?
- "Hey Timmy, how many pens do you have on your desk?"
- "Let's count them! Zero, One, Two."
- "So how many is that?"
- "Well since I ended my counting sequence with 'two', it totally makes sense to announce that I have three pens."
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.
> 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.
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.
With memory slots is the pointer in between or at a slot?
Should pages in books also start at 0? If you are saying show me page 4 should you look between 3 and 4?
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.
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.
The brain is very good at finding patterns for special cases and very bad at following repetitive orders.
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? :-)
Answer: none
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.
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.
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...
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.
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...
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.
That's what it represents in indexing, the distance you have to travel to get to the element you want.
That's an offset. It's already covered in detail in TFA.
We don't deal with such offsets in programming, except in C pointer arithmetic (which infested this upon us).
How many more things can I fit in that box? How many comments are in that HN page?
I'd write more, but I'm running on empty and my pen is out of ink.
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.
Closed and right-open intervals in Ruby:
(1..12).to_a
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
(1...13).to_a
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] |-1..12-|.to_a ## e.g. months in a year
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
/1..\13.to_a ## e.g. hours from 1h (1AM) to 13h (1PM)
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
It requires compiler support, but hey, you said a new language. for i in range(len) # 0, 1, ..., len - 1which is why switching between rust and nim is a real pain in the buttox
1..12 = [1,12]
With no syntactic notion for half-open intervals.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: Character range 'a' ...; -- perhaps?I am doing my best in teaching my kids this way but the world around has the wrong idea. We'll see how it goes...
We should be careful not let such easily observed facts inhibit our efforts to dumb down curriculum.
"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.
for x from 0 to 10 ;; closed, [0,10]
for y from 0 below 10 ;; (half-open, [0,10)
"below" is unambiguous, as 10 is not below 10.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 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.
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.
Maybe kids don't use rulers now? Just tell them that arrays are like rulers.