Wrapping And Forwarding Multi-Returns

Yup, sounds really useful.

One question, what do functions see?

f: function [a][]
f multi-return

Does f get the return as a block, or the first value?

What happens with multiple parameters in f? With refinements? With variadics? When f is enfixed?

That was six questions. :slight_smile:

UPDATE: The compacted history of multi-returns answers how it evolved from a hacked prototype into the isotopic PACK! solution of today... people should read it!

A Huge Question: Forwarding

Retroactively summing up the answer to this is that since a is an ordinary parameter, the PACK! isotope would "decay" and it would only receive the first value. But if it was a meta-parameter as ^a it would get a quasi-block containing all the multi-returns.

But what kind of argument does RETURN take. Does it take its return value as ^META and forward the multi-return values, or does it "decay"?

my-wrapper: func [arg1 arg2] [
     return multi-return arg1 arg2
]

It might seem nice to have the wrapper forward the returns by default, and use an explicit request to decay if that was not desired:

multi-wrapper: func [arg1 arg2] [
    return multi-return arg1 arg2  ; returns PACK! isotope
]

mono-wrapper: func [arg1 arg2] [
    return decay multi-return arg1 arg2  ; just returns first PACK! item
]

But I'm concerned this could lead to counter-intuitive situations, where this would be a multi-return:

 return some-function

...whereas this seemingly-innocent transformation would turn the code into a mono-return:

 x: some-function
 return x

It feels like you'd want some kind of signal before having these act differently.

3 Likes

What I've tried for a while to address this was to add a /FORWARD refinement to RETURN. If you don't use the refinement then it decays to just one value. (This decay also means raised errors decay to trigger actual runtime failures, etc.)

But this adds a creeping complexity, where the concern starts showing up other places. If you're doing a CATCH with a THROW, do you need THROW/FORWARD?

By contrast, the approach of assuming you want forwarding in such constructs lets you use a common DECAY primitive without contaminating them all with extra refinements. In the scheme of things, I'm not sure that an accidental leakage of multi-return information is obviously a worse default than the accidental loss of multi-return function.

There may be ways that a type signature can help here: if your RETURN: spec says you return an integer and you try to return a PACK!, it could automatically decay and type check the decayed form. (Though this wouldn't apply to the likes of CATCH and THROW, so that should be kept in mind.)

As for the decay on assignment... it may come down to making people comfortable with the idea that if you go through a non-meta assignment to a single variable you may lose information. You always have to read x: some-function as x: decay some-function and appreciate that potential for data loss. If you don't want to lose data:

x: meta some-function
return unmeta x