Reverting UNTIL, and adding WHILE-NOT and UNTIL-NOT


#1

Long ago, there was a fervent debate about whether UNTIL was a word that was “naturally paired” with WHILE…like IF and UNLESS. Hence, should UNTIL 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.


I’ve come to believe the interim solution should be the final solution. Hence UNTIL is reverted to its previous meaning, with WHILE-NOT being an arity-2 native, and UNTIL-NOT being an arity-1 native.


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.

Plus I didn’t wind up liking loop-while and loop-until as much as I thought I would…and I actually didn’t end up minding while-not or until-not.

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.


Note: The reason for having WHILE-NOT and UNTIL-NOT natives isn’t just performance (though they are faster than having to run each condition through a Rebol NOT in the evaluator). They also avoid a precedence issue you get in the new workings of things like equality tests, where while [not x = 10] [...] is read as while [(not x) = 10] [...]. If you use them, you can take advantage of the fact that your condition is “already grouped” by its block, and not worry about parenthesizing it further.

(Note that interpretation of not and = is the common one in languages with precedence… = is lower than ! in C for instance, and you’re expected to use !=. So it shouldn’t surprise most people, but since it’s not coming from precedence, see more about why it happens in: TO NOT B OR TO NOT (B)…is…no longer a question)


#2

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.

#3

Given the enfix covenant, this is no longer true either. And the performance difference is negligible; certainly not the most important thing to be focusing on…as opposed to just making NOT faster systemically!

So as the knife falls on other superfluous changes…so it does on WHILE-NOT and UNTIL-NOT. But doing so leads me to think it’s quite a good thing to get rid of these distractions, to focus on the much more consequential, much more obviously good changes…including things like the loop control protocol!