UPARSE was an early client of multiple return values, at a time when they worked by assigning variables local to your frame, which were then proxied to items in a SET-BLOCK!:
/multi-returner: func [
return: [integer!]
secondary: [integer!] ; SET-WORD! indicated another return
][
secondary: 20
return 10
]
>> [ten twenty]: multi-returner
== 10
>> ten
== 10
>> twenty
== 20
This basically made every multi-return function a kind of infix operation, that was able to take a SET-BLOCK! on its left hand side. (In fact, it was prototyped using infix.)
But this method had composability problems, and was defeated by abstraction of any sort, even the most minor forms:
>> [ten twenty]: (multi-returner)
** Error: even this wouldn't work
So the method gave way to returning antiform BLOCK!s. These represented parameter packs that would "decay" to their first item in most circumstances...but SET-BLOCK!s were one of the cases that could pick them apart (though you could design other operations as well).
/multi-returner: func [
return: [~[integer! integer!]~]
][
return pack [10 20]
]
You can read all about it in The History of Multi-Return in Ren-C
So Local Proxies Died...But UPARSE Mimicked Them
Just because the mechanics got rid of local proxies doesn't mean you can't fake them. All you have to do is hack up its RETURN function to make a PACK using a local variable.
Simplified example:
/proxy-multi-func: adapt func/ [
body: compose $() inside body '[
/return: adapt return/ [
atom: pack [(unmeta atom) secondary]
]
(as group! unbind body) ; I wish this pattern were simpler
]
]
/multi-returner: proxy-multi-func [
return: [integer!]
<local> secondary ; could be specially marked, if spec rewritten
][
secondary: 20
return 10
]
So when the multi-return-by-antiform-block change happened, this is what COMBINATOR did instead of transition to having every combinator do return pack [synthesized remainder]
Instead it worked the same as before: you'd set remainder however you wished, do return synthesized. Except now the specialization of RETURN would PACK things up.
Why Did COMBINATOR Preserve Proxying?
Well... for starters, to show that it could be done. You should be able to do it. So having a living test case to hammer through any issues was good.
Also, because some combinators have two return values (synthesized and remainder), while others add a third (pending). In truth the combinator always needs to return a pack of 3, it's just that some combinators automatically pipe the pending results from successful combinators to the output. This means even if your combinator returned a pack of 2 in the piped case, that would have to be broken apart and turned into a pack of 3. Having it in components helps.
But generally, I think it makes the code clearer as well. Saying (return pack [x y])
doesn't have any labeling, while (remainder: y, return x)
is somewhat clearer, and you don't need to label the "primary" result because that's understood as what the combinator is synthesizing.
Synthesized Can't Be Proxied (unless ^META)
It's worth pointing out that there's a sort of design constraint here, when you're going to break out multi-return results and have them represented by local variables which are proxied by an adjusted RETURN...
...and that constraint is that you can't put unstable antiforms in variables. So if you have something you want to return like an antiform pack (as combinators can legitimately synthesize), it has to be the main return result.
So since they use this proxying, combinators kind of break the rule of thumb of "don't make unstable antiforms your primary return in a multi-return situation". This is because if you do:
return pack [pack [1 2] "a"]
Then you face some ambiguity in terms of what people might think ([x y]: multi-return-func)
should mean... or what (x: multi-return-func)
should mean.
But really, this is still being worked out.
Anyway, Just Wanted To Sum Up UPARSE RETURN
I was questioning it, and wanted to kind of work through why it is the way it is. But I think it's right.