What should evaluating an empty block (`EVAL []`) do?

(Related question: What should do of empty string (`DO ""`) do?)


I've pointed out that the answer for things like "what should a loop that never runs its body" have varied.

rebol2/r3-alpha>> type? while [false] ["whatever"]
== none!

red>> type? while [false] ["whatever"]
== unset!

But it's consistent historically that if something runs do [] (what Ren-C calls EVAL when it's a BLOCK! you're evaluating) then you get an "UNSET!"

rebol2/r3-alpha/red>> type? do []
== unset!

New Antiform States Available

Ren-C has a choice most corresponding to UNSET!, which is TRASH. Trash does not display in the console, which might be considered an advantage.

>> eval []  ; imagine this returns TRASH

But if you evaluate to trash during something like REDUCE, it would give you an error.

>> reduce [1 + 2 ~ 3 + 4]
** Script Error: Invalid use of ~ antiform

To get opt-out behavior, it would have to give VOID or NIHIL.

>> eval []
== ~void~  ; anti

VOID would then be the same thing you get from a failed conditional.

>> if false ["a"]
== ~void~  ; anti

As well as the established result of an ANY or ALL in which all the expressions opt out.

>> all [if false ["hello"] comment "world"]
== ~void~  ; anti

Can anyone think of a case where there's a balance of provable value for something like a do compose [...] whose contents have all boiled away to be TRASH instead of VOID or NIHIL?

The historical rule is that there are no vaporizations by default in REDUCE. If you have N expressions in, you will have N values out. There is a religiosity about it in Red.

If you think reduce [eval []] should be [] then that would require both EVAL [] to be VOID and REDUCE to vaporize them:

 >> if false ["not"]
 == ~void~  ; anti

 >> reduce ["Bear in mind this will" if false ["not"] "make you a Red heretic"]
 == ["Bear in mind 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.)

I don't know how pivotal the particular point of eval [] is...

I don't really think we have code examples where this comes up all that often, which is probably why it hasn't been given that much thought.

OTOH the REDUCE default behavior does come up, I just 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", and the restriction hasn't bothered me 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.

1 Like

That actually arises from a more universal rule about GROUP!:

"groups just group things, they don't synthesize values of their own."

I tried changing eval [] to be void (and related places where empty blocks had to have an answer) and I don't really see any particularly obvious bad side to it. Nothing crashed.

So we're through the looking glass then? I think it's the right thing to do, we'll see...

I'd be interested to dig into this a bit more: "Redbol languages are based on denotational semantics, where the meaning of every expression needs to have a representation" / "I suppose he hasn't read Godel, Escher, Bach". The first statement seems quite inflexible and possibly restrictive.

1 Like

(Note: I've pared the above conversation down to the minimum, updating it with 2024 terminology and behaviors.)

There's a few interesting developments that inform the thinking on this...

Passing TRASH to Comparison Operations Now Fails

>> if (10 = get/any $asdf) [print "Not legal in Ren-C"]
** Script Error: = expects [something?] for its value2 argument

This decision aligns with Rebol2's treatment of UNSET! in comparisons (though Red and R3-Alpha allow it).

FUNC returns TRASH unless you use an explicit RETURN

A side effect of this is that it's a lot harder to accidentally return VOID from a function:

 whatever: func [x [block! group!]] [
     if block? x [append x spread [a b c]]
     if group? x [append x spread [d e f]]
 ]

Under the historical behavior, if you pass in a GROUP! the final function result would be the branch result of the APPEND which will be the group with [d e f] added. But if you passed in a BLOCK! then the IF GROUP? test would be false and the overall return result would be a VOID.

VOID's Properties Are the Same as They Have Been

  • Legal in comparisons

  • Illegal in isolated conditional testing

  • Opts-out when testing conditionally in aggregate (e.g. ANY and ALL).

FUNC Change Means The World Overall Is More Full Of Trash

In this case it's a good thing. The fewer accidental VOIDs that are being produced in the ecology overall, the better I feel about constructs like EVAL being willing to produce them, or for things like REDUCE (or anything else) being willing to discard them.

Honestly, It's Barely Come Up... Yet

Which is an indication it doesn't happen on accident. So the main way it's going to come up is if people know that's the behavior, and start designing code that purposefully uses the pattern.

And if they do that, then that is a good thing, because they're getting use out of it?

Since I'm trying to rig up the Big Alien Proposal (and the .WORD to select members proposal), I'm forced to spend some "quality time" with Rebmake. I'm actually happy to say I'm thrilled at how much clearer the once foreign-to-me-looking code is.

But I found at least one possible reason why EVAL of empty block (or branch) might should be TRASH by default. (and NIHIL if you give it a EVAL/VANISH refinement or similar)

Devil's Advocacy: eval [] as TRASH :imp:

Sometimes you write a switch or case statement, and there's nothing in the case:

switch x [
   'foo [
       ... do a bunch of stuff ...
   ]
   'bar [
       ; comment about how this is handled elsewhere
   ]
   'baz [
       ... do a bunch of other stuff ...
   ]
]

So what I'm seeing here is a situation where a branch produced, well, nothing.

I'm imagining someone coming along to a multi-page SWITCH or CASE (like the ones in Rebmake) and deciding to take the result of the branch and use it. And if they don't realize one of the branches was meaningless, then if we give them back VOID it can wreak havoc.

VOID opts out of lots of stuff. Luckily with VOID-in-NULL-out it can't propagate too far, but even one level of propagation can be confusing.

Despite This Wrinkle, I Still Think VOID Wins

Let's say you're writing something like this:

compose [a b (either condition ['c] [print "skipping"]) d e]

You have a branch that is running, and doing something... but you want it to effectively disappear. But if you don't do something about the TRASH coming back from PRINT, COMPOSE will choke on trying to put the non-void antiform it in the slot.

One way to deal with this is:

compose [a b (either condition ['c] [print "skipping" void]) d e]

Generally speaking you're going to want a comma there, especially if it was something more unfamiliar than PRINT, to emphasize it isn't an argument:

compose [a b (either condition ['c] [print "skipping", void]) d e]

And sure... you could do that. :confused: But I think the rhythm is measurably better with:

compose [a b (either condition ['c] [elide print "skipping"]) d e]

Well there you go. A proven use for the "unsafe" behavior.

(I like finding such cases so at least we know the decisions about these things aren't being made at random.)

There is now a NOOP function, that returns TRASH.

So if you don't want a branch to return VOID you can put a NOOP in it.

I'm still a bit nervous about returning void for empty branches. It's not that having branches that return void bothers me, it's that we're assuming a meaning for "no code at all".

I almost feel like maybe there should be a prohibition on empty branches: you have to at least write [~] instead of []. But... if we're going to assume an answer for empty blocks... that answer likely has to be VOID.