"back, back, next, next..." on returning to the original spot

Historically, the way Rebol deals with NEXT and BACK is that it clips it to 0 and the tail of the series...but does not error:

>> s: "abcd"
== "abcd"
>> s: next s
== "bcd"
>> s: back s
== "abcd"
>> s: back s
== "abcd"
>> s: skip s 3
== "d"
>> s: next s
== ""
>> s: next s
== ""
>> s: next s
== ""
>> s: back s
== "d"

I'm rather certain this is a bad invariant, but the question is what it should do instead.

One thing to notice is that this doesn't prevent the creation of series that are past their tail. The way Rebol implements ANY-SERIES! cells, it is possible for such cells to be made.

>> s: "abcd"

>> s4: skip s 4

>> clear s

>> mold s4
== {""} ;-- raises an out of range error in Rebol2

>> tail? s4
== true ;-- returns true in both R3-Alpha and Rebol2

>> insert s "ghijkl"

>> mold s4
== {"kl"}

With mechanics as they are, it is possible to guarantee no series cell ever has a negative index in it. But it's not possible--in the face of modifications of series through other cells--to ensure that a series cell won't have an index that is past the length of a series.

What I feel like should probably happen is that if BACK is asked to go behind a zero index, that it return a BLANK!. This is friendlier than erroring...but would cause errors down the line if one tried to treat it as a series and select elements from it.

If it does that, then NEXT probably should too when going past the length. It's a little uncomfortable when past-end-of-series cells can be made to exist other ways, to not just let NEXT keep marching into positions which may someday become valid (you might be planning on putting information there later). But with BACK blanking, the common usage expectation would probably be to return blank as well.

This would be nice for things like while [pos: next pos] [...] (or while [pos: my next] [...] if I figure out how to make ME and MY work...)

Opinions?

So, you'd prefer blank instead of errors on skipping past the end so you can use while but what if your series contains blanks?

pos: [ 1 2 3 _ 4 ]

then that's not going to work.

Whereas

while [ not error? pos: next me ]

should. Of have a version of while that stops the loop on errors?

while-good [ pos: next me ]

Why isn't it going to work? NEXT will return a series (if not tail) or blank (if tail)—the values at the index are immaterial.

>> new-next: func [series [series!]][                                      
[   either tail? series [_][next series]
[   ]
== make function! [[series return:][
    return: make function! [
        ["Returns a value from a function." value [<opt> any-value!]]
        [exit/from/with (context of 'return) :value]
    ] (
        either tail? series [_] [next series]
    )
]]

>> foo: [a b c d]                                                          
== [a b c d]

>> foo: head while [foo: new-next foo][probe foo]                          
[b c d]
[c d]
[d]
[]
== [a b c d]
1 Like

Right.

So the only thing that kind of makes me wonder about this is that it's based on specializing SKIP with 1 and -1. And this means that if you are just trying to skip as far back as you can and want to clip to the head or tail, you wind up having to add in logic to do that.

But I think you should have to be explicit about that. SKIP needs some kind of parameter for this.

It suggests maybe a SKIP*, NEXT*, and BACK*, but the problem here is that the blanking behavior is more fundamental. Because you can't just look at the output of an operation that wound up at the head and use that to indicate something "needs to be blanked". Yet I want SKIP and NEXT and BACK to be the ones that blank on out of bounds, out of an excess of caution (did you really mean you wanted to clip, or are you confused about what you are doing?)

So it probably needs to be a refinement to SKIP, which NEXT and BACK will inherit:

  • SKIP/CLIP, SKIP/BOUND, SKIP/STOP
  • BACK/CLIP, BACK/BOUND, BACK/STOP
  • NEXT/CLIP, NEXT/BOUND, NEXT/STOP

I mention above that it kind of breaks my expectation of what SKIP/ONLY (usually the meaning for things like SKIP*) would mean.

It isn't necessarily the case that NEXT and BACK need to follow the same rules as SKIP. It could be that SKIP has a lower level SKIP* that returns blank when out of bounds while SKIP itself clips. Then NEXT and BACK can never clip, so if you want the clipping behavior you use SKIP of 1 or -1 to make that clear, since that's "just how the higher level skip is". :-/ This would probably be less disruptive, as it may be the case that people are relying on SKIP's clipping historically...while unlikely to be relying on NEXT or BACK clipping.

I'll point out also that I noticed SKIP takes a LOGIC! in Rebol2 and R3-Alpha (Red did not keep this). It seems that a logic false will do the skip, while logic true will not. (??) If anything that seems backwards, I can kind of understand wanting skip s logic meaning either logic [next s] [s], but having it mean either logic [s] [next s] strikes me as wrong.

Today I went BACK one too far and REN-C console crashed on my when I wanted to get the series to display itself.

So I thought perhaps it is a good idea to have a BACK? and a NEXT? that first check if it is possible to go back or forward and if so the action is performed and if not, it will be skipped.

head? doesn't work?

BACK? would be effectively UNLESS HEAD? series [BACK series]
But I think
back? series
looks way better than
if not head? series [back series]
Don't you? Same for NEXT? protected from running beyond tail.

this way "back back next next" can return to the same spot even going out of bounds in the middle of the expression where nothing will be actually referenced. With the BACK? and NEXT? making sure of staying within bounds but NOT guarantying to return to the same spot in above situation.