What should do of empty block (`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 [] then you get an "UNSET!"

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

New Names Available

One of the benefits of having lots of different labeled BAD-WORD!s is to stop collapsing all "potentially error-triggering situations" into the same uninformative name.

>> do []
== ~empty~

Or maybe that's considered to be where the concept of VOID comes from:

>> do []
== ~void~

In either case, you've still got a BAD-WORD! that will be "ornery" and neither true nor false, that (probably?) errors if you try to access it. But by not reporting ~unset~ you're helping to convey that this wasn't the by-product of encountering an unset variable somewhere. It might help people get their bearings more easily.

(I've mentioned some concern about canonizing English into the evaluator mechanics...but maybe it's okay)

Or...Is This A Job For No Value At All (NULL) ?

We have some cases where emptiness produces pure NULL..the non-valued state. With DELIMIT and its specializations, an all empty block produces the same thing that an all-NULL-producing block produces:

>> if false ["a"]
; null

>> unspaced [if false ["a"] if false ["b"]]
; null

>> unspaced []
; null

>> unspaced compose [(if false ["a"]) (if false ["b"])]
; null

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 NULL instead of ~void~ ?

You could get the same result by saying do compose [null (...whatever...)] so it's not far out of reach to have a default value of anything you like.

A post was split to a new topic: The Naming of NULL and VOID!

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.

Ren-C has one avenue for bending it with invisibles like ELIDE and COMMENT. But if something returns NULL...which has no in-block representation--the current way of preserving the rule is to default to an error:

>> reduce [null]
** Script Error: [null] needs a value, can't be null

That's because the "default predicate" prohibits nulls.

But you can supply your own, asking REDUCE to do different transformation on your results before the new block is made. For instance you could negate values:

>> reduce/predicate [1 + 1 2 + 2] :negate
== [-2 -4]

So similarly, you could ask the predicate to turn nulls into "real" things, e.g. BAD-WORD!s:

>> reduce/predicate [null] :reify
== [~null~]

Or give it the non-erroring IDENTITY predicate which will remove the error behavior:

>> reduce/predicate [null] :identity
== []

If you think reduce [do []] should be [] then that would require both DO [] to be NULL and REDUCE to vaporize them:

 >> if false ["not"]
 ; null

 >> reduce ["Bear in mind this will" if false ["not"] "make you a Red heretic"]
 == ["Bear in mind this will" "make you a Red heretic"]

The risks of choosing it as a default come when you are building blocks and you run up against something like a select that failed when you didn't know it was going to, throwing off your positions. I felt it was a legitimate concern, so I didn't mind hiding it behind an option.

I don't know how pivotal the particular point of do [] 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 do [] to be null (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.

(2023 Note From @hostilefork: I have weighed in on how there's a way in which I agree every expression must have a representation, but that you may have to use an operator to get it... e.g. everything must be "meta-representable", this is the foundation of the isotopic age...)

1 Like

Hmmm...well when I tried bootstrapping the updated executable, here is an example of where nulls not erroring bit me when I changed some BLANK!s to NULLs:

It was some of Shixin's code from rebmake.

    if not let suffix: find reduce [
        #application target-platform/exe-suffix
        #dynamic-library target-platform/dll-suffix
        #static-library target-platform/archive-suffix
        #object-library target-platform/archive-suffix
        #object-file target-platform/obj-suffix
    ] project/class [return]

    suffix: second suffix

I had changed the suffixes in the base class of some objects from BLANK! to NULL. This was in order to be more likely to catch usage problems of those suffixes, when BLANK! is more quiet about many operations (e.g. they will silently append, like classical #[none] would).

NULL provides a gentle sort of alarm...in the sense that it is falsey and can't be e.g. silently appended without an operation converting it to a value. This is good for callsite comprehension.

But with NULL vanishing here, code in this style has problems. I'm not sure there's anything particularly wrong about code in this style. So we still might want to think about this.

To be clear, I was being a little glib with regard to the absolutism. I think if the explanation is sound, then it doesn't matter exactly which semantic model it adheres to.

A post was split to a new topic: What should do of empty string (DO "") do?

Update: "Isotopes" Have Changed The Game

One blind spot in the above discussion is that it assumes NULL is the only variable state that cannot be put in blocks.

The attempts to make suggestions other than NULL for do [] give "ornery" values like ~void~...but in a world that still allows those values to be put in blocks. :roll_eyes:

But with isotopes, there's a whole spectrum of states that are legal in variables but not blocks. We can very simply say that in constructs like DELIMIT or COMPOSE or REDUCE:

NULLs error, VOIDs vaporize, and the MAYBE function lets you convert nulls to voids!

And I've just done some work to bring it to bear on answering the age-old question of what any [] and all [] should be... in a way that brings back a whimsical-but-powerful feature that was tried in the early days of Ren-C...

First let's ask: what should this set RESULT to?

code: []

result: all [
    2 = 1 + 1
    do code

If it makes it any easier to think about, does the following influence your answer?

code: [comment "does this make you change your answer?"]

result: all [
    2 = 1 + 1
    do code

Should result be <item>, or NULL, or should the code error?

I've argued pretty clearly that DO must return a result that would be put into a variable, rather than vanishing unexpectedly.

But if that value is a void isotope, it is something that is known not to be able to be put into a block...and it is also something that intrinsically carries the idea of being a proxy for invisibility. "I'd be invisible if I could, but I didn't think it was safe to say so, by breaking things like assignments.

I think ANY and ALL are the kinds of construct that become more interesting if they vaporize ~void~ isotopes...and if ALL returns a ~void~ isotope itself if its contents all vaporize!

The idea this brings back is the concept of having a third option for value-returning functions... so truthy, falsey, and "opt out". But opting out is not conflated with nullness or unsetness, and it doesn't force unsafe "vanishing" semantics.

voter1: func [] [return true]
voter2: func [] [return ~void~]
voter3: func [] [return true]
voter4: func [] [return false]

>> all [voter1 voter2 voter3]
== #[true]  ; voter2 "abstained"

>> any [voter2 voter3]
== #[true]  ; voter 2 didn't make -or- break it

>> any [voter2 voter4]
== #[false]  ; voter 2 didn't make -or- break it

So voter2 gets to have invisible intent without the interface risks of being purely invisible (like COMMENT or ELIDE are). You can't opt out of everything...

>> if voter2 [print "IF expects a yes-or-no"]
** Script Error: if needs condition as ^META for ~void~ isotope

So to answer my original question, should you find a DO that returns invisible intent, that would be preserved...and even propagated through the ANY and ALLs themselves...

>> all ["A", all [comment "hi", do []]]
== "A"

>> any [all [do [comment "hi"], elide print "Magic!"], 1 + 2]
== 3

Although the all [] disappeared above, this is specific to the situation. We don't have a dangerous situation like:

>> block: [comment "hi"]

>> x: all [do block], y: 1 + 2

>> x
== 3  ; !!! <-- this would be crazy!

Instead we are letting the isotopes be semi-reified (reified enough to be stored in a variable, but not a block, without being de-isotoped). Then special cases handle them.

It's a new art!


I've done some pruning of dead-ends and tangents in this thread, to try and hone in on the real issues. But this is the central point:

In the intervening two years...things have evolved to where VOID is not an isotopic word, but a distinct non-valued state (which has its own quoted and isotopic forms). And it's not just the product of do [] but also any failed conditional.

>> do []  ; since void has no representation, console prints nothing

>> if false [<a>]

>> if true [<a>]
== <a>

There's a lot of satisfying interplay, in terms of how isotopic voids are used to represent unset variables...and quoted and quasi voids are the single-character intents of ' and ~. Singing the praises of the details would be redundant on this old thread, but I hope I've pared it down to where one can grasp the thought process that led to the current abilities.

At This Point I'm Sure About (do []) Returning VOID


Still Not Sure If (reduce [if false [<x>]]) Should Vaporize

There's evidence suggesting that some of the motivating cases for Red's religiosity no longer apply to Ren-C. e.g. set [a b c] reduce [expr1 expr2 expr3] is not how we operate... SET-BLOCK! and parameter packs are much better and account for voids

But I don't know about whether the default should be reduce/exact vs. reduce/vanishable. The feedback here suggests a lack of concern about the lax behavior, and the void vs. null distinction certainly puts a lot more control in the hands of the user.

Just wanted to come back and paint a picture of how far this has come!

1 Like