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.

However, I think I found a kind of good reason why EVAL of empty block should be NOTHING by default. (and NIHIL if you give it a EVAL/VANISH refinement or similar)

The thing is that 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.

The safety on EVAL of a constructed block is questionable.

But the safety on branching looks compelling to me. And it ties into some of my instincts about why it's important to bring back NULL as the untaken branch result for things that are more elaborate than IF.

Do notice that still, if your evaluation makes a void, you will get the void:

>> eval [if false [<unreachable>]]
== ~void~  ; anti

So it's really just the case of empty or all commented out that gives you the nothing.

>> eval [comment "like this"]

>> eval []

>> eval [elide print "Sitting target Sitting waiting. Anticipating..."]
Sitting target. Sitting waiting. Anticipating...

...Nothing.

I think the prior consensus may have been correct on this one.

If you want an EVAL to be able to vanish, then I think EVAL/VANISH is the right choice.

(Calling it /VANISH instead of /VANISHABLE may be a bad idea giving brevity instead of clarity. But for some of these words I feel like you either look them up to know what they mean or you don't. People who don't know what it does won't have any better idea from /VANISHABLE than they would from /VANISH. Or so my theory goes. Suggestions of better names welcome.)