February 2020: Fake It (with Infix) Until We Make It
It occurred to me to work up a prototype completely in usermode using a skippable infix parameter:
Multiple Return Values Via Infix
So a function could act as having multi-returns by peeking to its left... and seeing if there was a SET-BLOCK! there. If there was, then it would do the SET-ing of the variables that were needed (and could choose to only do calculations if variables were requested). If no SET-BLOCK! was on the left, it could just return a value as usual.
But this was not generalized in a way that functions could share.
April 2020: Unify With The Historical Refinement Model
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.
But since this mechanism existed, why couldn't the evaluator build a bridge so that the variables in the SET-BLOCK! would be passed in to specially marked refinement slots?
In April 2020, I implemented that. It made variables passed from a SET-BLOCK! on the left compatible with the historical method of passing WORD!s via refinements...to be SET by the function as additional outputs....
This comment from the C code commit explains how that prototype was approached.
//==//// SET-BLOCK! //////////////////////////////////////////////////////==//
//
// The evaluator treats SET-BLOCK! specially as a means for implementing
// multiple return values. The trick is that it does so by pre-loading
// arguments in the frame with variables to update, in a way that could have
// historically been achieved with passing a WORD! or PATH! to a refinement.
// So if there was a function that updates a variable you pass in by name:
//
// result: updating-function/update arg1 arg2 'var
//
// The /UPDATE parameter is marked as being effectively a "return value", so
// that equivalent behavior can be achieved with:
//
// [result var]: updating-function arg1 arg2
So all you needed to do to get the feature was mark a refinement as an output parameter. Then check to see if it's null or not, and assign it if it's a WORD! or PATH!...the same way you ever would have.
You can use it in the old style (like a TRANSCODE/NEXT being passed a position to update) or you can use the SET-BLOCK! syntax and let the evaluator do the magic.
That meant these two calls would appear equivalent to the insides of TRANSCODE:
>> value: transcode/next/relax "1 [2] <3>" 'next-pos 'error
>> [value next-pos error]: transcode "1 [2] <3>"
We're also given the feature of being able to check for if a return is requested, and vary the behavior based on it:
>> transcode "abc def ghi"
== [abc def ghi]
>> [value rest]: transcode "abc def ghi"
== abc
>> rest
== [def ghi]
TRANSCODE and LOAD seemed to show great results immediately (though later problems of composability from this feature ultimately lead to it being panned to use in core functions
)
And this was tried going along for the next year...