If you think reduce [eval []] should be [] then that would require both:
EVAL [] to be void, and
REDUCE to vaporize voids
Like this:
>> if 1 = 2 ["not"]
== \~\ ; antiform (void!)
>> reduce ["This will" (if 1 = 2 ["not"]) "make you a Red heretic"]
== ["This will" "make you a Red heretic"]
(It would still be possible to use REDUCE:PREDICATE and use a predicate function that errors on voids.)
In practice, I don't tend to use REDUCE that often compared to COMPOSE. reduce ['x: 1 + 2] seems awkward compared to compose [x: (1 + 2)]. So it's like the only time I would use REDUCE would be to build the "block of precisely N values".
Hence being restricted to N-in, N-out hasn't bothered me personally so far.
I hadn't pondered the absolutism of REDUCE. I've embraced your concept of vaporization for UNSPACED/SPACED/COMPOSE and don't see why REDUCE would be different. I notice that Ren-C (in R3C as well as current) does vaporize in the case of reduce [( )] which to mind is the same thing.
I think the right balance is struck by REDUCE not being "afraid of ghosts"... and erasing VOID! (light void) but erroring on empty PACK! (heavy void)...
The initial motivation for "afraid of ghosts" is being worried about a last eval result vanishing, due to something incidentally being a VOID!:
^x: eval [1 + 2, ^y] ; you would nearly never want to assign 3 to X
But REDUCE was following the pattern of COMPOSE. And I refuse to say that COMPOSE is afraid of ghosts, because I absolutely require the following:
>> compose [a (if 1 = 2 ['b]) c]
== [a c]
IF isn't a vanishable function, so using the AFRAID_OF_GHOSTS evaluation mode would mean that would create a "heavy void". And COMPOSE doesn't erase heavy voids. So this is not negotiable.
"Why doesn't COMPOSE erase heavy voids, you ask?" It might seem useful...for instance, you could pipe vanishing intent out of a branch that did some other work.
>> n: 1
>> compose [a (switch n [1 [print "one", ~()~]) b]
one
== [a b] ; useful? :-/
But that would be abandoning the safety mechanism that heavy void is trying to give you. Think again about the "y is incidentally ghost" situation:
compose [a (eval [1 + 2 ^y]) b]
With an infrastructure that's there to protect you, why would you throw away the protection at the last mile when you're composing in a slot?
You can still vanish that...and with minimal risk of distortion. An operator that turns heavy voids to light voids--but does nothing else--solves it cleanly:
compose [a (elide-if-void eval [1 + 2 ^y]) b]
(Shorter names for ELIDE-IF-VOID are welcome.)
REDUCE Is Better If It Is Also Fearless, but Firm
It's important that REDUCE be able to erase ghosts. It needs to erase ELIDEs and COMMENTs without decoration, for example. And it shouldn't erase heavy voids (without intentional use of something like ELIDE-IF-VOID)
So given that it erases ghosts, should it be willing to erase IF without decoration? Or should it be afraid, and require an operator?
>> reduce ['a if 1 = 2 ['b]]
** PANIC: were REDUCE afraid of ghosts, IF makes heavy void, error
>> reduce ['a ^ if 1 = 2 ['b]]
== [a]
On balance, I think I've come to believe you shouldn't need the operator by default. The reasons I refuse it in COMPOSE would equally motivate me to refuse it in REDUCE if I used it more often. But you do need the ^ in things like EVAL, or ANY, or ALL (for the reasons I outlined in my linked post).
Of course, people are allowed to disagree and make their own REDUCE. You could wire up your REDUCE to use a DECAY predicate, and it would error.
(Note: I haven't decided if REDUCE:WITH might be a better name, and I've also suggested that REDUCE/NEGATE might pass a predicate...as well as that the predicate refinement might take its argument positionally first... lots of questions there!)
This leads us to ask: Did it do an evaluation on the BLANK! and get a VOID!--and it ignores all voids? Or did it skip the evaluation step entirely on the BLANK?
One might think it would be "wasteful" to evaluate a COMMA!, and REDUCE should just skip it. But there is a question of RebindableSyntax... should you be able to change what commas do? Or are they exempt from overriding?
Vanishing Mixed With Non-VOID!-Taking Predicates
One also has to ask about how hard it should be to use a predicate function that doesn't take ghosts, with things like COMMENT or ELIDE:
But if NEGATE handles VOID! at all, one would expect it to return a NULL antiform, which REDUCE wouldn't be able to put in the block.
So that means one of the following needs to be true:
REDUCE always ignores VOID!s and doesn't pass them to the predicate
There might be some :GHOSTABLE refinement to say it passes them to predicates.
But COMMA! might need to be literally ignored even if you use :GHOSTABLE, otherwise if things like PACK were implemented as REDUCE:GHOSTABLE with a :PREDICATE of LIFT, you couldn't say pack [10 + 20, comment "hi"]
REDUCE does datatype detection on the predicate, and if it accepts VOID! then it passes the ghost to the function...otherwise it does not.
I'd Been Trying (2), But That Has Fallen Down
I've always been a bit suspicious of the technique of testing the predicate function for voids.
It keeps breaking--for one reason or another. Most recently when I switched to the ability to Bypass Type Checking By Quoting Typespecs. Because it meant that if you used it with a function that did that, it would typecheck anything, so all functions with optimized type checking would receive ghosts...even if their typespec said they didn't.
That revealed a bit of an oversight--which is to say that if a function ignores its typespec when you run it, that doesn't mean the PARAMETER! you receive back from it should also be in "ignore" mode. Extracting parameters needs to flip the bit back to "it gets checked".
So it was fixable. But if you find a part that keeps giving you troubles, that's a bit of an indictment.
Tempting To Just Throw Out Voids
I will point out that R3-Alpha's REDUCE had several extra refinements:
USAGE:
REDUCE value /no-set /only words /into out
DESCRIPTION:
Evaluates expressions and returns multiple results.
REDUCE is a native value.
ARGUMENTS:
value
REFINEMENTS:
/no-set -- Keep set-words as-is. Do not set them.
/only -- Only evaluate words and paths, not functions
words -- Optional words that are not evaluated (keywords) (block! none!)
/into -- Output results into a block with no intermediate storage
out (any-block!)
I think trying to make REDUCE be all things to all people seems like a mistake.
Offering VOID! to predicates is very rarely what you want. I've found one case (PACK). I could reasonably argue that if you're "weird" you should do the evaluations yourself...you've got EVAL:STEP and other lower-level tools.
(and in the case of PACK, it can actually reuse REDUCE's native implementation, twisted with an "act like PACK" flag.)
So I Guess I Was Mistaken (with default REDUCE, at least)
Under this set of premises, REDUCE:PREDICATE won't see voids, at least by default. I'd imagine that REDUCE-EACH would probably use the same rule.
That seems the sane default. If down the road people complain enough, REDUCE might get a :GHOSTLY or :GHOSTABLE refinement. Until then, use EVAL:STEP to see ghosts.
(Unless we decide that EVAL--too--should skip voids by default and have a :GHOSTABLE refinement. But that sounds impure to me, I really doubt it.)