[Language construct idea] The for loop with prepare step

H. S. Teoh hsteoh at qfbox.info
Mon Feb 12 17:52:20 UTC 2024


On Mon, Feb 12, 2024 at 03:58:50PM +0000, Quirin Schroll via Digitalmars-d wrote:
> Extend `for` to `for (init; prepare; condition; increment) body`.
> 
> The step `prepare` is executed before each check. Ignoring scope, the
> above is lowered to:
> ```d
>     init;
> start:
>     prepare;
>     if (!condition) goto end;
>     body;
>     increment;
>     goto start;
> end:
> ```
> `continue` is also `goto start`.
> 
> Sometimes, you want to execute something before each condition check,
> no matter if it’s when entering the loop or when looping back.
> 
> D’s best options for that is:
> * Repeat yourself in `init` and `increment`, possibly using a local
> function if the step is more than 1 simple instruction.
> * Use `{ prepare; return condition; }()`. While not a DRY violation,
> this has the disadvantage that you cannot declare a variable in
> `prepare` that’s present in the loop’s `increment` or `body`.

The primary issue here is that the conventional looping constructs (the
age-old for, while, do) enter the loop at the top, and exit the loop at
the bottom. However, your general loop may exit the loop in the middle.
(The other possibility -- entering the loop in the middle -- is
equivalent to this case via a simple code rearrangement.)  Therefore,
you have this structure:

	loopStart:
		loopBodyBeforeCondition();
		if (cond) break; // loop exit
		loopBodyAfterCondition();
		goto loopStart;

When loopBodyBeforeCondition is empty, you have a while-loop. When
loopBodyAfterCondition is empty, you have a do-loop. When
loopBodyAfterCondition only contains incrementing code, you have a
for-loop (the initialization part of a for-loop technically belongs
outside the loop, just before it begins).  However, there is no direct
equivalent to the case when neither loopBodyBeforeCondition nor
loopBodyAfterCondition are non-empty (and the latter isn't just for
incrementing). In such cases, most code resorts to hacks like
`while(true)` with manual exit points.

One example of the usefulness of a loop construct that supports the
above construct in full generality is the outputing of list delimiters.
If we postulate a syntax construct like:

	loop {
		loopBodyBeforeCondition();
	} while(cond) {
		loopBodyAfterCondition();
	}

then, given some range r of items, we can output it with delimiters like
this:

	loop {
		writef("%s", r.front);
		r.popFront;
	} while(!r.empty) {
		write(", ");
	}

However, since we don't have such a construct, we have to resort to
various hacks, like:

	while (!r.empty) {
		writef("%s", r.front);
		r.popFront;
		if (!r.empty)	// <-- non-DRY
			write(", ");
	}

Or:

	string delim;	// hack: need an extra variable
	while (!r.empty) {
		writef("%s%s", r.front", delim);
		delim = ", "; // hack: lots of redundant assignments
		r.popFront;
	}

Or:
	if (r.empty) return;
	writef("%s", r.front);	// hoist first iteration out of loop body (non-DRY)
	r.popFront;
	while (!r.empty) {
		writef(", %s", r.front); // fold loopBodyAfterCondition into front of loop
		r.popFront;
	}

The asymmetry of these hacks is caused by the underlying
non-correspondence between the desired structure of the output
(delimiter is placed between every pair of items) and the available
structure of looping constructs in the language (none of the existing
looping constructs exit in the middle of the loop body).

Having a construct like proposed above solves the problem by making
available a construct that has a 1-to-1 correspondence with the desired
structure of the output.


T

-- 
Genius may have its limitations, but stupidity is not thus handicapped. -- Elbert Hubbard


More information about the Digitalmars-d mailing list