We've fretted a lot about the result of REDUCE when an expression produces something that can't be put in a block. At first this was just NULL. But now it's all BAD-WORD! isotope forms.
One reason to fret is the historical idiom of setting multiple values in a block. This has been done with a SET of a REDUCE. Something along the lines of:
>> x: 3 + 4 == 7 >> set [a b] reduce [10 + 20 x] == [30 7] >> a == 30 >> b == 7
It's your choice to reduce the values or not. If you don't involve REDUCE then the mechanics would all work out. But once you get NULLs and isotopes, the reduced block can't store the values to convey them to the SET...
But What If A Single Operation Did Both...?
Let's imagine we have instead something called UNPACK that by default reduces. Imagine it quotes a SET-BLOCK! on its left.
>> x: 3 + 4 == 7 >> [a b]: unpack [10 + 20 x] == 30 >> a == 30 >> b == 7
UNPACK manages the evaluation one expression at a time, instead of using REDUCE. So as it goes it can set the variables to NULL or isotopes. And by following the multi-return convention of returning the first value, you avoid ever needing to synthesize a block aggregating all the results together.
>> [a b]: unpack case [ 1 = 1 [ print "This is pretty slick..." [select [a 10] 'b, 1 + 2] ] ] else [ print "This won't run because the ELSE gets a BLOCK!" print "Which is what you want, because the ELSE should be" print "what runs if no CASE was matched and have the option" print "of providing the block to UNPACK" ] This is pretty slick... ; null >> a ; null >> b == 3
Quoted BLOCK! can Even Avoid A REDUCE
If you already have a block in reduced or literal form, how would you tell the UNPACK about that? It could be a refinement like UNPACK/ONLY. BUT...what if we let quoted blocks signal it?
>> [a b]: unpack the '[1 +] == 1 >> a == 1 >> b == +
Remember that there's more than one way to get a quoted block. I used THE as a literalizing operator (since '[1 +] would lose its quoting level if evaluated). But I could have also used the ^ forms or JUST and gotten the same thing:
[a b]: unpack ^[1 +] [a b]: unpack just [1 +]
A real aspect of power in this approach is the ability to mix and match. For instance you could have some branches in a CASE which have already reduced data and others that don't, and they could all participate with the same UNPACK operation.
[op1 op2]: unpack case [ ... [ print "This branch uses values as-is" ^[+ -] ] ... [ print "This branch needs evaluation" [operators.1, pick [- /] op-num] ] ]
Cool Dialecting Twists
It seems to me nice to safeguard that you're not throwing away values:
>> [a b]: unpack [1 2 3] ** Error: Too many values for vars in UNPACK (use ... if on purpose)
As the error says, though, we could indicate we're fine with this through a special syntax:
>> [a b ...]: unpack [1 2 3] == 1 >> a == 1 >> b == 2
(It's not as sketchy when you have too few values, because you can set the extra variables to unset...which will presumably trigger awareness of a problem at some point.)
I think the idea of "circling" values to say which is the one you want the overall expression to evaluate to is a neat idea
>> [a @b]: unpack [1 2] == 2
But that will have to wait until I can rig back up the @ forms.
And For Show And Tell...
How hard is it to write such a thing, you ask? In Ren-C it's super easy, barely an inconvenience:
unpack: enfixed func [ 'vars [set-block!] block [block! quoted!] ][ let result': ~unset~ reduce-each val block [ if vars.1 = '... [continue] ; ignore rest, but keep reducing if tail? vars [fail "Too many values in UNPACK (use ...)"] if not blank? vars.1 [ set vars.1 unmeta ^val if unset? the result' [result': ^val] ] vars: my next ] if vars.1 = '... [ if not last? vars [fail "... only at the tail of UNPACK vars"] ] else [ for-each var vars [ ; if not enough values for variables, unset if not blank? var [unset var] ] ] return unmeta result' ]
If the ^ and UNMETA seem confusing, the only thing you need to think about is that the META protocol helps you out when you're trying to deal with a situation of storing a value that can be anything...and you need to differentiate a state. I'm making the result "meta" so that I can use plain unset to signal that it hasn't been assigned yet. I could make a separate boolean variable instead, but then I'd have another variable and I'd have to GET/ANY the result...
I'm sure people will start getting the hang of it!