The first cut at multiple return values made a multi-return output look like:
func [/foo [<output> integer!]] [...]
Which was expedient to try it out. But dialect-wise, I think a SET-WORD! makes more sense:
func [foo: [integer!]] [...]
This would take away SET-WORD! as a way of denoting locals in the frame. This was to make it easier for higher-level generators to stick values in the frame without having to search for a
<local> annotation and inject at the right place. Programmatic code generating frames could stick locals anywhere they wanted in the parameter sequence, which was helpful.
If we're thinking that's still a useful idea, then func [a b: c] [...] could be rethought e.g. as func [a .b c] [...].
I will point out that while this sounds convenient, any higher level generator that just splices words into specs like this can run into problems of name reuse...since locals cannot clash with parameters.
But What About The Deviant RETURN: ?
The only "local" most users noticed in specs was RETURN:. Strangely enough, yes, it was a frame local variable...that held the definitional RETURN function.
It's a bit confusing to have RETURN: work different than other things specified by SET-WORD!. This could be avoided by using a different convention, e.g.
<return> [integer!] to stress the difference.
Or we could just say that it's "one of those things" and learnable, that the act of naming a return value "RETURN:" implies it's the main return...and gets special conventions and handling.
Why Aren't Returns Written Directly To Frame Slots?
Something that may seem confusing about return values is that you don't assign them with SET-WORD!s, you assign them with SET.
foo: func [return: <void>, multi: [integer!]] [ multi: 10 ; **wrong** (should be an error, assigning read-only cell) set multi 10 ; **wrong** (multi is NULL if caller didn't want it) if multi [cheat: get multi] ; **wrong** (variables voided prior to call) if multi [set multi 10] ; CORRECT: detect desire for output ]
(Note that you can't GET a multi-return input variable, because the variables are voided prior to the call. This avoids the possibility of "outputs" acting as inputs.)
I've made the point that this allows us to tie in the request for an output with a difference in semantics for the function... where using
# is a way of asking for the semantics without also needing to give a variable to store the result in.
This design makes even more sense when you consider that they are made to be compatible with the passing of words that you can do historically. Without this, there'd have to be some kind of parallel-universe of interacting with them in things like APPLY.
apply :foo [/multi 'var] ; what would the syntax be otherwise?
Really...as weird as all this is, it's at the same time familiar.
We could imagine a different setup which tried to let you just do multi: 10 directly, and then when the operation was over would proxy that value into the target variable. And it could use some similar rules about how when the frame started, it could be either NULL if it wasn't wanted or
# if it was wanted. But that seems a lot more error-prone. And the variable exists anyway to make the request...so why not go ahead and set it where it is, instead of going through a middleman anyway?
What To Do About Input/Output Name Collisions?
To bring an end to voidification, I need a channel of communication set up between branching constructs and subsequent enfix operators...so they know if the branch was taken or not.
I've rigged up something very hacky, but good enough to get me to the next big question: what happens when the subsequent enfix operation is treated as a branching operation itself.
Could a parameter be converted into an "in-out" form? /branched: perhaps?
else: enfixed func [ return: [<opt> any-value!] /branched: [logic!] ; hypothetical syntax value [<opt> any-value!] ][ ... ]
But this is a bit of a can of worms. You could have a situation where branched wasn't passed as an input -nor- requested as an output...where it was only one of the two...or where it was both. How are these states conveyed, and how do you react to them?
It's difficult not to think that this kind of parameter would best be handled as being set to the value on input (or NULL if no input), and then whatever value it holds at the end of the frame is its final result. That's either proxied to an output variable if requested, or not if it isn't. But now you've lost the ability to tell if the output was requested or not...because that NULL state is taken for no input provided.
Could it be that there are two frame variables tied together, with the left giving the canon name for both the input and the output parameter, and the right as an internal alias for the output parameter?
else: enfixed func [ return: [<opt> any-value!] /branched.branched-out: [logic!] ; hypothetical syntax value [<opt> any-value!] ][ ... ]
This overlaps a bit with something I've spoken about, which are cases where you'd rather the name you use on the interface be distinct from the name you use for something inside the frame. Like if you named a refinement /ALL but you plan on using ALL [...] in your code, it would be nice to havae a notation to say you wanted a different name:
So one might think it could be solved in that way; you'd rename one (or both) of the /BRANCHEDs out of the way
else: enfixed func [ return: [<opt> any-value!] branched.branched-out: [logic!] ; want output renamed value [<opt> any-value!] /branched [logic!] ; just use same name for input ][ ... ]
Seems nice, but this problem is fundamentally different. Because a function like APPLY still has two possibilities for what /BRANCHED is fulfilling. Our problem isn't just with internal naming, it's at the level of the interface itself not being able to call an input and an output the same thing.
Although... APPLY could have syntax for that.
apply :foo [ /branched true ; input form, no colon /branched: 'var ; output form, colon ]
That feels like it sucks and it's making things feel more painful than they already are. And while it might work for APPLY, there's no such syntax for calling in a normal path invocation.
So The Names Have To Be Distinguished On The Interface?
If the input and output aren't somehow fused into one argument, then yes. Distinct frame slots would need different names:
else: enfixed func [ return: [<opt> any-value!] branched-out: [logic!] value [<opt> any-value!] /branched-in [logic!] ][ ... ]
But that just gives the question of what would motivate the evaluator to automatically tie together an input to an output with a distinct name.
Maybe there's a positional thing that could be done with it, the way modal parameters do it. If you mark a parameter in a certain way, it will assume a relationship to an adjacent declaration
else: enfixed func [ return: [<opt> any-value!] /branched: [logic!] ; or some syntax marking "output has input too" /branched-in [logic!] ; since it follows a `/x:`, assumed linked value [<opt> any-value!] ][ ... ]
That's About The Best I've Got
I've satisfied myself that the input and the output should not wind up stuffed into the same frame slot. The input should be a normal refinement, and have either its value or NULL. The output should be through a level of a WORD! indirection (or NULL if not requested).
I think I've made a pretty good case that since both slots appear on the public interface, it's not a good idea for them to have the same name. You wouldn't be able to address them individually.
Most constructs will not have to worry about this.
It's a very rare category of function that would have to use this technique. Take note that things like FIND and SELECT are designed to work within the framework where NULL is not a "meaningful value"...all they need to do is return NULL and they interoperate with ELSE.
If you write a normal non-enfix control structure, all you need to do is have a normal
branched: output that you set in a straightforward way.
It's only enfix tools that are branching structures in their own right that have to pay this complexity tax. But what we're saying is that it's better for them to pay it, so that we aren't having to contort every IF or CASE or SWITCH statement just to appease them. It's localizing the concern into the place where the complexity belongs, vs making the whole rest of the system pay for it just so ELSE and THEN can be expressed simply.