UPARSE Combinator Return Conventions: Reviewed

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 '[
          return: adapt return/ [
              ^value: pack [^value secondary]
          ]
          (as group! body)
      ]
 ]

 multi-returner: proxy-multi-func [
     return: [integer!]
     {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: you'd update the input position however you wished, do return synthesized. Except now the specialization of RETURN would PACK things up for you.

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 updated input), 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.

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.

So the question of "how to show local proxies in the function spec" comes up... and I wonder if using SET-WORD! is a good idea or a bad one. (?)

Additional SET-WORD!s have no meaning to FUNC, but to a COMBINATOR that's generating a FUNC it can pick any convention it wants...since it's mutating them into locals.

It's not the worst concept, to use SET-WORD!s. But it's a bit weird that RETURN is a function while SECONDARY is just the name of a local that gets proxied into the results, that you assign via normal assignment.

One horrible suggestion would be to use a leading slash on RETURN to denote its distinct status as a function:

 multi-returner: func [
     /return: [integer!]
     secondary: [integer!]
 ][
     secondary: 20
     return 10
 ]

That's a "cure is worse than the disease" idea if I ever saw one.

Since the output arguments manifests as local, maybe it could go inside a FENCE!?

 multi-returner: func [
     return: [integer!]
     {secondary: [integer!]} 
 ][
     secondary: 20
     return 10
 ]

That's clearly bad, as it looks like it's assigning the BLOCK! to secondary vs. type-constraining it.

Maybe the CONSTRUCT dialect is liberal and leaves a few things open to interpretation vs. trying to define everything... like TAG!, maybe?

 multi-returner: func [
     return: [integer!]
     {<secondary> [integer!]} 
 ][
     secondary: 20
     return 10
 ]

Eeerrrrhhhg. :roll_eyes:

It may be a good idea to not completely saturate CONSTRUCT's dialect space so that higher level constructs can get creative in augmenting FENCE!. But I don't think this is a clarifying step up from using a SET-WORD! in the outer scope, even if that looks "like a return".

HEY, Wait. I Have a Winner...

 multi-returner: func [
     return: [integer!]
     {secondary}: [integer!] 
 ][
     secondary: 20
     return 10
 ]

"I am a local, but I am also used as a proxying output."

That's my favorite so far. It helps shift it into another category where your expectations are different than of a return function.

:trophy:

Should This Be A Built-In Feature?

Above I'm writing that as if FUNC understands it, when in reality I was talking about a feature that was specific to COMBINATOR.

I don't think it makes a lot of good sense to try and put meaning on that by default. Because as the COMBINATOR case shows, the "main return" actually becomes the second output.

It's easy enough to write your own proxying function generators that I don't know how much value making one that's in the box has. But if there is, it should probably be called MULTIFUNCTION (MULTIFUNC) or something like that.

I guess you could put things in whatever order you wanted:

 multi-returner: multifunc [
     {secondary}: [integer!] 
     return: [integer!]
 ][
     secondary: 20
     return 10  ; returns pack [20 10]
 ]

Coolio.

1 Like