COUNT-UP replacing REPEAT, and add COUNT-DOWN

@giuliolunati requested a counting-down version of REPEAT, originally suggesting REPEAT/BACK. I said that didn't seem particularly clear to me, and suggested COUNT-DOWN instead:

>> count-down i 3 [print i]
3
2
1

I might have suggested calling it COUNTDOWN un-hyphenated....except it seems it would go nicely with an upward counting version...and COUNTUP doesn't look right... it needs the hyphen:

>> count-up i 3 [print i]
1
2
3

This touches upon an old gripe of mine, which is that I felt like there was nothing about the name REPEAT which suggested it would be modifying a variable. The idea that LOOP would be arity 2, and REPEAT would be arity 3, made no intuitive sense to me--I thought if anything, REPEAT should be the one that did things N times with no parameterization:

>> repeat 3 [print "no parameter, just repeat"]
no parameter, just repeat
no parameter, just repeat
no parameter, just repeat

Years ago I'd made that suggestion because I wanted to repurpose the word LOOP. The concept was that LOOP could become to iteration what PARSE is to parsing. It was pointed out to me by @JacobGood1 that LOOP is what LISP uses for their "looping dialect".

It seems to me that with COUNT-UP and COUNT-DOWN, we have a pretty strong justification for why it has a parameter to update as it counts. Because if it didn't have a parameter, there'd be no difference between counting up and counting down.

Hyphenation is always kind of a bummer, but for what it's worth there were only 4 or 5 instances of REPEAT in the Ren-C codebase...between Mezzanines and make scripts and everything else. It may not be the biggest deal in the world. I think retaking the shortest word LOOP for higher purposes is pretty important by comparison.

Sound good?

Yes, sounds good for me.

This sounds ok and fairly easy to try on for size.

So far I'm liking COUNT-UP and COUNT-DOWN, having used them.

I noticed one feature that got sacrificed, which is that REPEAT doesn't just do counting. It can also do a variant of FOR-NEXT (e.g. FORALL) where you name the series.

r3-alpha>> repeat x [a b c] [print mold x]
[a b c]
[b c]
[c]

This is probably a little-known feature, as Rebol2 treated it the same as FOREACH:

rebol2>> repeat x [a b c] [print mold x]
a
b
c

Meanwhile, Red doesn't accept series to REPEAT at all:

red>> repeat x [a b c] [print mold x]
*** Script Error: repeat does not allow block! for its value argument

What about ITERATE, what might that mean?

I have a fuzzy memory of suggesting ITERATE before for something. What may make the most sense would be if ITERATE acted like classic FORALL as the forward iterating form that named no variable, and then the FOR-SKIP, FOR-BACK, FOR-NEXT all took variables and didn't change the original series position.

The reason I suggest that is because the old nature of FORALL makes it rather unsafe w.r.t. errors. If there's a failure, there's nothing to put your series variable back at the position it was originally. Also the name makes it sound a bit more like it mutates something, "iterate what you give it"...it's transitive-verb-y.

There's a few questions about how the series enumerator visit positions:

>> for-next x [a b c] [print mold x]
[a b c]
[b c]
[c]
// should tail [] be here?

>> for-back x back tail [a b c] [print mold x]
// should the `back` be necessary?  Should [] be here?
[c]
[b c]
[a b c]

>> x: [a b c]
>> iterate x [print mold x]
[a b c]
[b c]
[c]

>> count-up x 3 [print [x]]
1
2
3

>> count-down x 3 [print [x]]
3
2
1

>> repeat 3 [print "repeating"]
repeating
repeating
repeating

>> loop [...]
** We don't know yet, but whole block would be in "loop dialect"

These need to be thought through, but I think the single-arity LOOP that takes you into "loop universe" with a dialect is where the real payoff is on all of this.

Maybe I lack imagination on a Monday morning :coffee:, but I can't think of a good reason to print the tail position, even when molding the block. Just my opinion, but the tail position is one of the first things a newbie encounters in Rebol where the seams of the language is exposed in a strange way. I'm perfectly used to it, but it's a bit like the vertical blanking signal on your (standard) TV-- you learn "oh, there's an invisible thing there that is performing an important function"

Empty blocks are always at the tail position. So if we're going to have empty blocks, we have to mold blocks at their tail.

I guess the question is if for symmetry, if FOR-BACK runs a BACK before it runs the body of the loop.

>> block: next next [a b c]
[c]

>> for-back x block [print [x]]
[b c]
[a b c]

We'd have to reflect a bit on how this might relate to Rebol's "0 hole":

>> block: next next [a b c]
== [c]

>> block/1
== c

>> block/0
** Script Error: block/0 has no value

>> block/-1
== b

Maybe FOR-BACK thinks it has to essentially "skip the 0", and go straight to negative 1, while FOR-NEXT also skips the zero and goes straight to positive 1? :-/

While we're on the subject--I kind of don't like that FOREVER (loop with no condition) is called that, because it discourages usages where you want to style a loop that you plan to break from...e.g. the condition doesn't really fit at either the head or tail of the loop:

forever [ // oh, it's "forever"
    ...
    if condition [break]
    ...
]
... // if it was "forever", why is there code here?

You get this "forever...not!" situation that I think discourages the use of FOREVER. I know I don't use it very often. But really, a lot of loops might be clearer written like this.

So I'd perhaps argue that this use of FOREVER may be more common than looping a fixed number of times without knowing which time you're on. In which case it could perhaps deserve the name REPEAT more than that does.

Or here's another suggestion for a better name for forever... CYCLE.

It's 2-letters shorter and connects fairly well that it's something you can only exit if you BREAK-the-CYCLE.

I'd feel more comfortable using it if it were called that than FOREVER, do others agree?

Maybe FOREVER could be like CYCLE, except actually not offer a BREAK (though it would then also have to trap all THROWs or RETURNs or any means of exiting...and then error and exit the process!)

One aspect of CYCLE that is kind of unfortunate is that since the only way to exit it is with a BREAK, if it follows the "break is null" protocol you have no information coming out of it. Because it always breaks. This may justify an exception to the rule...where it returns the last value the body evaluated to. Or perhaps it justifies one case where BREAK could take an argument?

Perhaps the difference in semantics warrants a special STOP operation that does take a parameter, which only CYCLE offers. Then BREAK would still follow the rule (returns null) and STOP would require a non-NULL argument.

1 Like