So the COLLECT keyword in Red is--in @rgchris's opinion (and mine) basically useless. Because backtracking does not undo the things you've collected.
red>> parse "ac" [collect [
"a" keep (<a1>) "b" keep (<b>)
| "a" keep (<a2>) "c" keep (<c>)
]]
== [<a1> <a2> <c>]
It got half way through the first sequence...matching an "a" and doing a keep, then realized it didn't match the "b". So it rolled back to the start and again matched an "a" and did another keep, then a "c" and did a keep.
It's far more useful if bookkeeping is done to backtrack what's being collected, as Ren-C does.
ren-c>> parse "ac" [collect [
"a" keep (<a1>) "b" keep (<b>)
| "a" keep (<a2>) "c" keep (<c>)
]]
== [<a2> <c>]
How Does Red Justify The Non-Rollback Behavior?
Maybe @rgchris can chime in here to say more precisely or find some direct quotes. But I believe the general justification is that they think of rollback as a "new" behavior that swims upstream from the fact that "code with side effects isn't unwound in general".
all-redbols>> parse "ac" [collect [
"a" (print "a1") "b" (print "b")
| "a" (print "a2") "c" (print "c")
]]
a1
a2
c
Nothing rolls back arbitrary code in groups...and Ren-C is not planning on being any different.
In this point of view, they would say that in the implementation of KEEP they are running an APPEND. They conclude it's thus reasonable to say that the user experience should correspond to not wanting there to be too much of a gap from the implementation. If there is, it would mean adding more code/design...
I call that the tail wagging the dog.
Anyway, this introduction aside... let's get on to the main part of what I wanted to write about.
The Mechanics Behind Rollback are Non-Trivial
One thing about adding something like COLLECT into the UPARSE combinator model is that it's the sort of thing that winds up having to know about what happens with the relationship of every other parser.
Imagine you have something like an EITHER combinator that will succeed if either of two rules succeed. This would be the ideal behavior.
>> uparse "aaa" [collect [either 2 keep ["a" "a"] 3 keep ["a"]]]
== ["a" "a" "a"]
Remember that a BLOCK! rule evaluates to its last result, and TEXT! rules against strings evaluate to the rule itself, so this is the expected answer.
I wrote this example above the way I did instead of putting the two arguments to this hypothetical EITHER in BLOCK!s:
>> uparse "aaa" [collect [either [2 keep ["a" "a"]] 3 [keep ["a"]]]]
== ["a" "a" "a"]
Because if I did it that way, then you might argue that the only place COLLECT needs to get its hooks in would be BLOCK! combinators. But we are saying part of the interesting natural-language property of PARSE is to (correctly) manage the implicit grammar...it's foundational to Rebol philosophy to not require the blocks (though to offer the option if the grouping makes it more clear).
So when I wrote it without the blocks it was to demonstrate a point. Accomplishing the rollback conceptually needs to have "hooks into the EITHER". It needs to know that although the overall EITHER succeeded it failed in part of its implementation. One of the 2 attempted keeps from the first rule succeeded but that turned out not be enough.
COLLECT must somehow thus get a notice on every combinator's call...effectively before to record the high water mark of the collection... and then after to know if it failed, to roll back.
This Might Sound Familiar...like an ENCLOSE
If you recall how ENCLOSE works.
hooked-append: enclose :append func [f [frame!]] [
print ["Hey, I know you're trying to append a" f.value]
print ["That append only happens if I DO this frame..."]
let result: do f
print ["I did the frame and got back" mold result]
print ["But I'm returning something else!"]
return <you got hooked!>
]
It's pretty nifty:
>> data: [a b c]
== [a b c]
>> hooked-append data 10
Hey, I know you're trying to append a 10
That append only happens if I DO this frame...
I did the frame and got back [a b c 10]
But I'm returning something else!
== <you got hooked!>
>> data
== [a b c 10]
This rather directly corresponds to what COLLECT needs, as a service. Except instead of enclosing one function it's adding some code to a hook in every combinator.
I'm going to have a bit more to say about this as I go. But I had noticed that in my first crack at thinking about where to put the rollback I'd made the mistake of putting it in the BLOCK! combinator. So I was motivated to write up a good example like the EITHER to show why that wasn't correct.
Also any chance to talk about Red as being (intellectually) lazy and anti-intelligent-design is fun.