I get what you are saying. If you write a "recognizer" with parser combinators (like a regex without capture groups) and then want to convert that recognizer to an actual AST parser, you have to change it more deeply than just choosing what pieces to capture. You have to carefully follow a trail of left/right capture/discards to figure out what actually ends up in a function input. It's tree-shaped not linear-list-of-captures.
Some parser combinator environments are better than others. Haskell's applicative style is pretty good:
(\x y z -> ...) <$> parser1 <*> parser2 <*> parser3
F#'s FParsec has pairwise operators `.>>`, `>>.`, and `.>>.` for capture-left-ignore-right, capture-right-ignore-left, and capture-both-and-tuple. These rapidly get unwieldy:
parser1 .>>. parser2 .>>. parser3 |>> (fun ((x, y), z) -> ...) // ugly nested tuples!
So they have functions like `pipe3` instead:
pipe3 parser1 parser2 parser3 (fun x y z -> ...)
Still, what a pain when you mix in some "ignored" parts in between the "captured" parts. Ugly and awkward to follow.
pipe3 (whitespace >>. parser1) (char '(' >>. parser2 .>> char ')') (whitespace >>. parser3 .>> whitespace) (fun x y z -> ...)
I wrote a library some years ago to alleviate my issues with that F# flavor of this problem. My solution is very similar to yours in that it also asks the user to think of it as like regex "capture groups"!
https://rspeele.github.io/FParsec-Pipes/Intro.html
My approach is more symbolic-operator flavored than yours though. In mine you always start a chain of parsers with `%%`, separate with `--` and end with `-|> function`.
%% parser1 -- parser2 -- parser3 -|> fun () -> ...
If you want to capture any part of the chain you use the unary prefix operator `+.` to mark it captured and it will get passed through to the consuming function.
%% +.parser1 -- parser2 -- +.parser3 -|> fun p1 p3 -> ...
I used it extensively in a SQL parser:
https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez...