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 Isotopic States Available

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

>> eval []  ; imagine this returns NOTHING

But if you evaluate to nothing 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 NOTHING instead of VOID or NIHIL?

The historical rule is that nothing vaporizes 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...

The State Held by Unset Variables (e.g. NOTHING) Is Now Truthy

>> if get/any $asdf [print "Ren-C considers NOTHING as truthy"]
Ren-C considers NOTHING as truthy

This decision aligns with Red's choice for UNSET! (though Rebol2 and R3-Alpha consider trying to conditionally test UNSET! as an error).

Passing NOTHING 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 NOTHING 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).

What Are The Implications For (EVAL []) ?

  • FUNC Change Means The World Overall Is More Full Of Nothing - 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.

  • NOTHING Is In Some Ways Less Ornery - if the goal of eval compose [...] when all the material vaporizes was to give back an "ornery" result, it's worth noting that NOTHING is less ornery than it used to be in conditionals (and more ornery than it used to be in comparisons).

    • Were you to write if eval compose block [print "Truthy"] and all the expressions in BLOCK vaporized when this wasn't your intent, VOID "offer more safety" now than NOTHING

    • But if you wrote if 10 = eval compose block [print "Truthy"] and all the expressions in BLOCK vaporized when this wasn't your intent, NOTHING would "offer more safety" than VOID

    I know this seems silly but that's my point. Choosing to return NOTHING for eval [] is useless (on purpose)... so its only theoretical value is to offer some safety on shooting yourself in the foot with VOID when something like a REDUCE'd or COMPOSE'd block decays to no elements. But that safety seems a bit of a mirage, and also it prevents you from exploiting useful VOID results when you want that from the decay.

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 NOTHING by default. (and NIHIL if you give it a EVAL/VANISH refinement or similar)

Devil's Advocacy: eval [] as NOTHING :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 NOTHING 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.)