Reverting UNTIL

Long ago, there was a fervent debate about whether UNTIL was a word that was "naturally paired" with WHILE...like IF and UNLESS. So it was wondered if UNTIL should be an arity-2 function which was effectively WHILE-NOT, instead of its arity-1 form.

The change was introduced in Ren-C to try it. But as an interim solution before the name was truly reclaimed, while-not was added. The intent was to go in and replace it with UNTIL later, after the change had settled. LOOP-WHILE and LOOP-UNTIL were the arity-1 natives.

To make a long story short: I guess I don't think that it wound up making that big a difference in the scheme of things. The primary impact on me personally, was actually just making code golf programs longer. (Having U be UNTIL is an appealing option.)

When you look at forever [...] and until [...], they seem to go together sensibly, and you might ask--as some have--if it's WHILE that's weird. If you think about WHILE and UNTIL as being two words offering entry points into loop space, it's like it's giving you two common ones with common words.

I didn't wind up liking loop-while and loop-until as much as I thought I would. And WHILE as arity-1 was also tried to give parity with PARSE's WHILE, but that was awful...and made it clear that arity-2 was the right answer for PARSE as well.

So here in the time of analyzing changes for goodness or badness and trying to pin things down, I think the status quo is the better answer. Plus, when there are so many other slam-dunk good changes, this one is more a distraction than anything else.

So DocKimbel/Rebolek/Oldes/etc. get to win on this one.

Here is the old argument for LOOP-UNTIL and LOOP-WHILE from the Trello, for historical reference.

The points are still pretty reasonable, and I don't think it was any kind of disaster. It just didn't move the needle very much...some things looked better, some things less good, it was near break-even. And with WHILE-NOT and UNTIL-NOT I think the mechanical arguments for having all 4 variants were covered.

There are simply bigger fish to fry than this particular change.

Historically Rebol has had a WHILE construct that takes two arguments...a condition block and body block:

x: 0
while [x < 3] [
    x: x + 1
]

The condition is evaluated first, before the body is ever run. Then alternately the body is evaluated and the condition re-evaluated until the condition comes back conditionally false (e.g. BLANK!, FALSE). At that point, the return result of the WHILE expression is the last evaluation product of the body. (3 in the case above.)

Under the hood it "takes turns" ramping up the evaluator twice each loop cycle on two different blocks. In a sense this can be seen as making the loop itself somewhat less efficient than if it had been chosen to be single-arity, and merely testing the result of a single block evaluation. Ren/C adds this more efficient construct as LOOP-WHILE:

x: 0
loop-while [
    x: x + 1
    x < 3
]

Using the prefix LOOP- is abstractly filling in the directive of "make a loop" or "turn a loop", to help inform what to do in lieu of any supplied body (the usual expectation being for while loops to have one). Other choices might be noop-while, continue-while, cycle-while etc. But LOOP- seemed the best prefix.

(Note: It is unrelated to what the command or dialect LOOP winds up meaning.)

The relative efficiency of LOOP-WHILE might lead one to ask if it deserves the WHILE name and should replace it entirely. There are several arguments for keeping the two separate constructs, plus keeping WHILE as-is:

  • LOOP-WHILE forces the test after the "body code"...so the "body" will run once before the test, which you may or may not want
  • LOOP-WHILE cannot return the last result of evaluating the "body" (which is usually more interesting than returning the result of evaluating a condition for a while, given the only possibilities for that are FALSE or BLANK!).
  • WHILE is ubiquitously used in arity-2 form in existing code...by nearly every person to ever use the Rebol language.
  • WHILE's visual separation of the condition from the body with separate blocks serves a communicative purpose which helps drive its popularity.

It's also worth noting that LOOP-WHILE may be more efficient when standing alone. But it's not more efficient if (for instance) you are dealing with condition and body blocks that aren't already in independent variables. The following will be measurably slower than just while condition body (and may behave differently, and return a different result!):

x: 0
condition: [x < 3]
body: [x: x + 1]
loop-while [
    do body
    do condition
]

This understanding of WHILE informs Ren/C's accompanying introduction of an arity-2 UNTIL to match:

x: 0
until [x = 3] [
    x: x + 1
]

It runs the condition before the first execution of the body, and returns the last evaluative result of the body before the condition became true.

There is also a LOOP-UNTIL:

x: 0
loop-until [
    x: x + 1
    x = 3
]

This makes LOOP-UNTIL a synonym for what Rebol2 and R3-Alpha just called UNTIL:

x: 0
until [
    x: x + 1
    x = 3
]

The reasons for retaking the name UNTIL are:

  • every new "non-guru" user (and many of the other "guru" users) found Rebol2's UNTIL to be an inconsistency of the words WHILE and UNTIL. They suggest a matched pair like IF and UNLESS.
  • A two-arity UNTIL is genuinely useful in its own right, for the same reasons that WHILE is, including bringing more efficiency to some scenarios than LOOP-UNTIL
  • A two-arity UNTIL is genuinely a natural fit for readability in many situations by separating the condition and body blocks.
  • Single-arity UNTIL's popularity "in the wild" is vastly lower than WHILE, making it less disruptive to change.
  • It is not difficult for codebases which worked with the old definition to work around it with a very simple until: :loop-until, pending a simple search-and-replace to fix the usages.