Thursday, January 13, 2011

Loopy thinking

Let's think about loops. The venerable father of loops (for most programming languages) is the old FORTRAN "DO" loop:

DO 200 I=1,10
C do stuff
200 CONTINUE

which performs the section "do stuff" ten times, changing the value of I from one to ten on each iteration.

BASIC uses a similar notation:

10 FOR I = 1 TO 10
20 REM do stuff
30 NEXT I

In C, the code is:

for (int i = 0; i < 10; i++)
{
/* do stuff */
}

(The loop in C runs from zero to nine, not one through ten, since C programmers start counting at zero.)

All of these expressions of a loop force the programmer to think in terms of the loop and the incremented variable. The variable exists for the loop, and its value changes with each iteration. Most programmers think that this is the natural way to think of loops, because this is what they have been taught.

But it can be different.

In C#, the code can look like C, or it can look like this:

foreach (int i in Enumerable.Range(1, 10))
{
// do stuff
}

This style has advantages. The variable is not calculated each time through the loop, but instead holds a member of a set of values (that set just happens to be calculated for the loop). This expression of a loop shifts the mental model from "initialize a variable, compare, do the loop, and oh yes increment the value" to "use the next value in the sequence". I find the latter much easier to understand; it is a lighter cognitive load and lets me think more about the rest of the problem.

Yet the C# implementation is a bit clumsy. In other languages, you can easily start and end with any limits:

DO 200 I=10,20
10 FOR I = 10 TO 20
for (int i = 10; i < 20; i++)

But in C#, the construct is:

foreach (int i in Enumerable.Range(10, 20 - 10 + 1))

The "20 - 10 + 1" bit is necessary because the arguments to C#'s Enumerable.Ranges specify the starting number and a count, not the ending number. Oh, I could write:

foreach (int i in Enumerable.Range(10, 11))

but when I scan that line I think of the set (10, 11) when it really is (10..20).

So I must award partial marks to C#. It tries to do the right thing by the programmer, but the implementation is wordy and clunky.

I much prefer Ruby's concise syntax:

(1..10).each |i| do
# stuff
end

No comments: