Users of languages that have a multiple return value feature seem to rave about it. And it's something that users from languages that don't have it will ask about, complain about, and constantly try to find ways to work around.
Even before an actual implementation of SET-BLOCK! existed, I had inklings that Rebol could do them in a way that is mindbending and original. But there were nagging technical and semantic concerns for every version of the idea I could envision.
Now I have to say that after having the opportunity to do experimentation with the enfix left-quoting hack for multiple return results... and seeing what Ren-C is capable of...
...multi-return is a must-have
I'm convinced we'd be wasting SET-BLOCK! terribly in the evaluator if [x y]: [1 2] was only for "setting x to 1 and y to 2". While I've been concerned in the past about consistency in naming with set [x y] [1 2]... we either forget worrying about that or just use something else to do that. e.g. assign [x y] [1 2], problem solved.
In terms of other problems that seem addressed comfortably: I knew that we needed to be able to return NULL to values in multi-return scenarios, but that would be a problem with even the reduced case if we tried to return a BLOCK! to match the SET-BLOCK! in size:
>> [x y]: function-returning-10-and-20 == [10 20] >> x == 10 >> y == 20 >> [x y]: function-returning-null-and-30 == [#[void] 30] ; can't put null, so... VOID! in first slot? >> x == null ; not the same thing as what the block said...seems bad, yes? >> y == 30
But when I disconnected the idea from the old behavior of set [x y] [10 20], the Gordian Knot was cut, and the evaluative result of a SET-BLOCK! had no need to be a BLOCK! with all the values...but merely one value. This made great sense, because since BLOCK! is always truthy, you'd not really be able to make useful conditional behavior be based on a BLOCK! return anyway!
>> [x y]: function-returning-10-and-20 == 10 >> x == 10 >> y == 20 >> [x y]: function-returning-null-and-30 == null ; so you could meaningfully say `if [x y]: whatever [...]` >> x == null >> y == 30
The question of whether it has to be the first value is an open one. If you could annotate the elements of the SET-BLOCK! that might be a way of saying otherwise. And perhaps the single return doesn't have to be coupled to the multi-value return case at all (?)
An Important Feature: Return Awareness
The old school "multiple return" method was to pass in WORD!s to set as variables. Such as DO/NEXT:
r3-alpha>> value: do/next [1 + 2 10 + 20] 'pos == 3 r3-alpha>> pos == [10 + 20]
You see that DO could check for the presence of the /NEXT refinement and behave differently. It knows whether it has one return value or two. Based on that knowledge, many routines might have more optimized implementations when not all the possible return results they could give are wanted.
Previously I phrased this with the example:
>> [x y]: sum-and-difference 20 10
Let's say that gives back 30 and 10. But if the user omitted return results, it might be possible to be more efficient:
>> x: sum-and-difference 20 10 >> [x]: sum-and-difference 20 10 >> [x _]: sum-and-difference 20 10 ; ^-- let's say all are equivalent ways of asking for the sum ; but not the difference. >> [_ y]: sum-and-difference 20 10 ; ^-- asks for the difference but not the sum
How would it be able to tell? It seems like it would be bad to give it the actual BLOCK! it's assigning into, because it could look at the prior values...and it feels like the words in the return block should not be used to pass information as a out-of-band input.
Thus perhaps a multi-return function gets a proxy block, which has VOID!s in it where variables are, and blanks where the blanks are. It's the multi-return's job to assign those slots, and then the evaluator takes care of mapping the values back.
A Huge Question: Forwarding
The premise this is operating on is that multiple return-valued functions are a fundamentally different beast, and any single value goes through assignment normally to the first slot:
>> [x y]: [1 2] == [1 2] >> x == [1 2] >> y ; null (or maybe void?)
This magical property of functions has a bit of a problem, though...
my-wrapper: func [arg1 arg2] [ return wrapped-function arg1 arg2 ]
If WRAPPED-FUNCTION has the property that it would assign multiply to a SET-BLOCK! target, then you'd lose that multiple-return-ness. Much like the need that motivated the existence of APPEND/ONLY, you can't have it both ways...a value like a BLOCK! can represents a single multi-valued item, or multiple values, but not both at the same time.
It seems clear there'd need to be some kind of operator for unpacking and repacking a multi-valued function's results into a BLOCK!.
>> one: multi-returns-10-and-20 == 10 ; drops 20, not requested >> both: unpack multi-returns-10-and-20 == [10 20] >> [x y]: repack [10 20] == 10 >> x == 10 >> y == 20
That doesn't help with the wrapping problem, though. You'd need some kind of forwarding return. Perhaps something like a return/pack?
It might seem nice to make forwarding the default, but this would lead to counter-intuitive situations, like these two being different:
return some-function x: some-function return x
It feels like you'd want some kind of signal before having these act differently.
So many questions open, but still I think this has to happen
I'm sure enough that I wouldn't suggest using SET-BLOCK! in the evaluator for anything else.
If we say we have to punt on it design-wise for a while and make it an error, then that's fine. It's just time to scrap the idea of [x y]: [1 2] setting x to 1 and y to 2. That's such a waste! So keep one's thinking hats on with this...