What should `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 labeled voids 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 "plain" form of void comes from:

>> do []
== ~void~

In either case, you've still got a type that will be "ornery" and neither true nor false, that 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 that this is a bit unfortunate in the sense of canonizing English into the evaluator mechanics. But I'm taking away the option by removing ~ as a form of void...which is what this case had been before.

Or Is This A Job For NULL ?

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

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

>> unspaced []
; null

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

There's some neat combining of this with PRINT. Although PRINT draws your attention to calling with NULL via error, a BLANK! will get it to overlook that and just be a no-op:

>> print unspaced compose [(if false ["a"]) (if false ["b"])]
** error, print doesn't take NULL

>> print try 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, 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" is .non.null

But you can supply your own, asking REDUCE to do different transformation on your results before the new block is made:

>> reduce .negate [1 + 1 2 + 2]
== [-2 -4]

If you use VOIDIFY then you will get ~nulled~ voids instead of an error:

>> reduce .voidify [null]
== [~nulled~]

And if you use IDENTITY then you overrule the default .non.null predicate, and since NULLs are okay they pass through as-is and will vaporize:

>> reduce .identity [null]
== []

Of course there's also TRY.

>> reduce .try [null]
== [_]

I don't know what the best defaults are, but here we see all available options, pretty much.

If you think reduce [do []] should be [] then you should put that together as a complete thought, in light of all the information presented here. That would require both DO [] to be NULL and REDUCE to default to the .identity predicate.

 >> 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.

We could also give these operations distinct names, if we can think of them.

The "make your own safety" philosophy sort of biases this kind of decision to where you let people decide if they're tripping across a certain category of bug, and if so build their own responses to it. As long as it's easy for people to use reduce .non.null [...] or specialize it that way, that might be te best answer.

If do [] returns NULL, should an empty function return NULL?

>> foo: func [] []

>> foo
; null

>> if foo [print "Does it matter this doesn't error?"]
; null

I don't know that I see a particularly strong case for why an error would be needed there. But again, this is running against some historical behaviors

 rebol2>> foo: func [] []
 rebol2>> foo
 rebol2>> if foo [print "Does it matter if this doesn't error?"]
 ** Script Error: if is missing its condition argument

Although in bizarro Red land, unset! is truthy:

>> foo: func [] []
>> foo
>> if foo [print "Red has truthy unsets, so no error here."]
Red has truthy unsets, so no error here.

If we're to glean any information out of that, it might be that there's no "must preserve" instinct on this particular avenue of error.

We no longer have to go down a line of reasoning which might have said if true [] needed to discern its result from if false [], so that tying into what do [] needs to produce to get that discernment. That was already suspicious, and the issue is attacked another way now.

Anyway, going with the null instinct is a less "conservative" choice because it doesn't draw attention to the "strange" case and say "did you mean to do that?". However, if nobody can think of a time when do [] returning UNSET!/BAD-WORD!/VOID!-what-have-you saved your butt, then it must not have saved your butt very often.

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's a more universal rule about GROUP!:

"groups just group things, they don't synthesize any VOID!s (or NULL) or artificial 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.

One thing that feels better is to have it show null in the console than to do the same thing that a "function without a meaningful return result" does;

>> print "There's nothing on the next line, and that's okay for PRINT."
There's nothing on the next line, and that's okay for PRINT.

>> do []

>>

Having nothing on the line after do [] is unsettling to me; it doesn't feel like you've uttered the incantation to declare the "no meaningful result" intention. I prefer:

>> do []
; null

>>

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.

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 protection offered by the .non.null has been fine with me so far.

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.

The one test that has me a little bit uneasy is this:

>> do ""
; null

On the one hand, being able to accept an empty string at face value as "hey, I meant that empty string" is part of the premise of the whole vocabulary of NULL/BLANK! in the first place:

  • print null and thus print unspaced [() ()] are errors
  • print _ and thus print try unspaced [() ()]] is a no-op (returning NULL)
  • print "" prints just a newline, assuming you meant no content for line
  • ...hence so does print unspaced [() "" ()] etc.

If you buy into all this, then instead of being paranoid about the historical ways in which empty strings might sneak in, you accept it for meaning what it does and cure the problem at the edges so that everywhere you have an empty string you mean it. You have NULL, BLANK!, and now a whole family of BAD-WORD!/VOID! to express other shades of meaning if you had them...not to mention all the other types (tags or issues, etc. etc.)

On the other hand, sometimes you are in a situation where strings are the only medium of exchange, and not reacting to the potential problem they represent feels a little wrong. Think for example interfacing with C or JavaScript...(although they both have NULL too, arguably...and rebValue(null) is an error because NULLs can't evaluate).

But maybe NULL is the right balance of reaction to this, and do "" returning NULL...instead of being paranoid prickly in some worse way is the coherent behavior of handling what you asked.

Again with make your own safety, anyone who finds themselves running up against problems where DO of empty string is bad can redefine DO in their uses to handle it any way they like...e.g. by failing in the call rather than returning a void:

do: adapt :lib/do [
    if (parse try match text! :source [any space]) [
        fail ["DO received all empty text input"]
    ]
]

>> do "   "
‌** Error: DO received all empty text input

Now that's The Sauce...


I was curious what the JS console in Chrome and Firefox did when you just hit "enter", and found it interesting to see that neither let you; it has no response. You have to at least hit "space". I think that's a bit frustrating in terms of not giving you a way to test the responsiveness. In any case, hitting space gives you the same response as eval("") which is undefined.

You don't get any particular safety from this:

>> x = eval("")
<- undefined

>> if (x) { console.log("doesn't complain on undefineds?"); }
<- undefined

JavaScript does actually consider "undeclareds" as different from "undefineds", where a variable explicitly assigned undefined doesn't error on access (only attempts to dereference). The "undeclared" state might be compared to "unbound".

Anyway, just something to hmm about.

1 Like

Both changes are now in master:

The new REDUCE behavior has been advocated now by @rgchris, @BlackATTR, @giuliolunati, @gchiu, and was my original choice also:

>> append [<a> <b>] reduce [<c> if false [<d>]]
== [<a> <b> <c>]

Up until now it has errored to leave the option open, without yet breaking the "N expressions in, N values out" dogma espoused by DocKimbel.

But now, we consciously say "seeya" to that noise. Though those who wish other policies can parameterize REDUCE:

>> append [<a> <b>] reduce .non.null [<c> if false [<d>]]
== [<a> <b> <c>]
** Error: NON failed with argument of type NULL

>> append [<a> <b>] reduce .try [<c> if false [<d>]]
== [<a> <b> <c> _]

And if they wish they can make that parameterization their default.

Just a reminder here of the existence of the NON function, which is the "anti-ENSURE", and cool...

3 Likes

So there's another set of equivalencies to consider here: how FUNC treats an empty body, vs. a RETURN with no argument, vs. a DO of an empty block. Current behavior:

>> one: func [] []
>> one
; null

>> two: func [] [do []]
>> two
; null

>> three: func [] [return]
>> three  ; ornery-but-not-printed-in-console result

Changing DO of an empty block to return NULL made ONE and TWO equivalent. But THREE still returns the ornery-but-not-printed-by-the-console thing (pending change: this would be the BAD-WORD! ~none~)

ONE and THREE Matching seems Important

If you showed this to a random person who didn't know the language, they'd probably have a stronger opinion on the equivalencies of ONE and THREE than an opinion about TWO.

But being a less-random person... I can see this as an edge case of not passing RETURN an argument that it has made a concession for you with. It isn't erroring. But it's not giving you a full pass...by saying you've made it "under-specified". So it's fabricating a hot-potato to make a note of that underspecification.

The Typing Issue Is Worth Considering

A good reason to make the RETURN case give back the meaningless result is that it's easy to return NULL if you want it:

four: func [] [return null]

But it's a bit more awkward to return the bad word:

five: func [] [return '~none~]

You could use this as an argument for saying that having RETURN around makes it easy to get the bad word if you want it, so the body being empty case should remain "pure" in the sense of acting like a DO of a block for substitutability purposes.

Quick Reminder: NULL vs. ~NONE~ is Interesting

Because ~NONE~ is notably not NULL, it can trigger THEN. PRINT uses this distinction in NULL to accomplish an interesting effect:

 >> a: "Something", b: null, c: "Something Else"

 >> print [a b c] then [print "We know we output!"]
 Something Something Else
 We know we output!

 >> print [b]
 ; doesn't output anything

The ability to notice when a PRINT statement was opted out of...either by BLANK! or all its content opting out, is neat.

Another option might be that RETURN would return the isotope NULL ("NULL-2"), which would still give this particular effect without the ornery-ness. But it wouldn't suppress console display, which is another nice feature.

I've Though Plain RETURN meaning "Invisible" is Sketchy

It seems that a RETURN with no argument meaning that the function has no return value at all would provide an answer to the loophole of "how do I signify the return of absolutely nothing".

You get the tempting ability to chain functions that are invisible, even if you don't know ahead of time that they are:

>> foo: func [action] [return action "Doing A Chain"]

>> 1 + 2, foo :print
"Doing A Chain"

>> 1 + 2, foo :comment
== 3

On top of my worries that was "weird", I also thought we might want to allow RETURN to take multiple arguments as a convenience...and having this feature wasn't compatible with that.

But things may have changed a bit now that we've gone to saying that invisibility means "void". We're taking a common understanding of what the term "void function is" and tying that to "really no value". So if return gave back really no value...it would be acting like a void function.

And also, I've found that the return @(...) form is really awkward.

So maybe it's not so weird after all, and we should just accept that multi-returns are another way.

The Right Answer isn't Plain RETURN Returning NULL ("NULL-1")

That leads us to either a choice between the current state, or making func [] [] give back ~none~ (or NULL-2, if RETURN gave that), or be invisible. :-/

I'm a bit torn over changing plain RETURN and empty function to be invisible. Something about seeing the pieces all laid out like this makes that feel like the most elegant solution, with an empty function body (or body of all invisibles) meaning "void" (new sense).

Where I'm having a little bit of buyer's remorse on this is actually not with DO, but with PARSE.

I've explained that I think we get a big leg up if we make it possible to use SET-WORD! in combination with a BLOCK! in a parse rule to mean "assign the set-word! with the result of the last value-bearing rule in the block". This aids in the separating-parse-rules across contexts, because you're bubbling out values...and doing so through the natural order of rules. This feels like a good direction.

But I've also mentioned that we have to then be careful and not make too many rules be "value-bearing" in a way that synthesizes results and then discarding them for no reason.

When these two things bump against each other, you're likely going to get cases of people writing:

parse ... [var: [thru ")"]]

...or something like that. So we don't want your garden-variety THRU to be synthesizing results and then just throwing it away, hence it's invisible.

We've done the PARSE parallel to assigning the result of an invisible in a GROUP! in DO:

do [..., var: (elide print ")")]

Something about having VAR be NULL in the parse case feels like you missed a chance to tell people something went wrong.

Having VAR be "ornery" (BAD-WORD!, VOID!, whatever we call it) seems like a sort of bare minimum way of getting a cue at the callsite. Erroring on the assignment seems like it would be the most helpful.

But maybe the problem here is looking at things like THRU being invisible, vs themselves being returners of BAD-WORD! etc. Perhaps that's "invisible abuse"; and only things that you are indoctrinated more obviously as invisible (like GROUP!, ELIDE) really return invisibly?

Maybe it's for the best that SOME and THRU return cheap garbage instead of be invisible, then you elide them:

 parse "((1))" [x: [some "(", integer!, elide some ")"]]

Food for thought. But I do think we need to be careful about rules synthesizing big unwanted values too easily.

We still would need to know what x: [] should do, which would be the same as what x: [(print "hi")] would do... since plain GROUP!s are invisible in parse rules.

Consistency here seems good. Assigning a parse rule that's empty or where everything vaporizes seems it should line up with assigning a DO rule that's empty or where everything vaporizes.

I'll also point out that cascading invisibility through a GROUP! in DO is a parallel problem to cascading invisibility through a BLOCK! in PARSE. It works in DO:

>> do [1 + 2, elide print "Hi"]
Hi
== 3

>> do [1 + 2, (elide print "Hi")]  ; group around invisible code
Hi
== 3

The group makes no difference there so. One would hope you could do a similar thing with a BLOCK! in PARSE:

>> parse [1] [return [integer!, (print "Hi")]]
Hi
== 1

>> parse [1] [return [integer!, [(print "Hi")]]]  ; block around invisible rule
Hi
== 1

e.g. if a BLOCK! contains all vanishing rules, then the last result is left as is.

This should work with alternates too:

>> parse [1 "a"] [return [integer!, [text! | (print "no text!")]]
== "a"

>> parse [1] [return [integer!, [text! | (print "no text!")]]
no text!
== 1 ; e.g. `[text! | (print "no text!")]` vaporized, leaving the 1

So I like how this stuff lines up. Actually implementing it...well...that's another matter.

I think I have a sound explanation for why DO resulting in invisibility should be an ornery value by default, but an invisible value when the caller requests it.

The main reasoning isn't so much about DO of a BLOCK! as it is about DO of a FRAME! that represents an attempt to reliably capture (and possibly tunnel or manipulate) an arbitrary function invocation.

If a caller is not expecting it, invisibility can cause confusion when running a DO:

f: make frame! :comment
f/discarded: "discard me"  ; the argument to comment

result: do f
foo: 1 + 2  ; example of code coming after this

If the DO behaved as invisible by default, we would wind up with RESULT being 1 + 2. Clearly bad.

For clients who know what they're doing, a refinement (something like /VOID or /VANISHABLE) is useful, so that the DO can actually disappear...if that is what they meant. This can come in handy for blocks as well.

But what I like about making it return ~void~ if used without /VOID is that it helps signal that information is being overlooked. You asked for a result, so you were presumably interested in the result. If the reason for using DO is writing some very generic code that operates on frames, then it's nice to have a tripwire to help you know when you've lost information in the exchange. Returning NULL would gloss over that.

I think it's probably useful to make a "poor man's vanish", where you just assume ~void~ should turn into disappearance.

>> vanish 1020
== 1020

>> 304 vanish print "PRINT returns ~void~, so vanish it"
== 304

This isn't a fully rigorous mechanism, you shouldn't use it when writing generic function-manipulating mezzanine-type things. DO is a function that absolutely needs a refinement to get it right. But it would be crazy if every function that called DO had to have such a refinement too.

Though I'm reviewing now whether the @ branches should speak the quoted protocol:

>> if true @[1 + 2]
== '3

>> if false @[print "Branch Not Run"]
; null

>> if true @[null]
; null

>> if true [null]
; null-2

>> if true @[]
== ~void~  ; invisibility signal

>> if true @[print "Returning BAD-WORD! with label `void`"]
Returning BAD-WORD! with label `void`
== '~void~  ; distinct from invisibility signal

This makes them less likely to be useful in a mix-and-match scenario, where one branch is @ convention and others aren't. But it's more informative than putting the @ around the IF as a whole:

>> @(if true [1 + 2])
== '3

>> @(if false [print "Branch Not Run"])
; null

>> @(if true [null])
== '

>> @(if true [])
== '~void~  ; quoted like other branch products

>> @(if true [print "Returning BAD-WORD! with label `void`"])
Returning BAD-WORD! with label `void`
== '~void~  ; looks just like `@(if true [])` result

But now that plain @ exists, it's not a lexical burden to quote inside a block if you want to. So the advantage of allowing @-branches is precisely that mix-and-match ability.

*Anyway, long story short: I think the void result is the way to go, with a special mode for getting invisibility at the callsite if you want it.

1 Like

For better or worse, here is an example of where nulls not erroring (or turning into BAD-WORD!s 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. now leaning to being a no-op for things like APPEND, but it is part of the "blank-in, null-out" convention many places).

So since BLANK! can be silent if it's not set when it's expected to be, NULL provides a gentle sort of alarm...in the sense that it is falsey and does not need special GET/ANY access to test it. And you can transition it to a BLANK! with TRY. 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.

Note: Under the new philosophy of BAD-WORD! this would be an interesting case, in the sense that if you allow it to put ~nulled~ into the block, you will wind up not immediately getting an error on it...because it will be "friendly" when it is picked via SECOND. It will just be a type that won't be tolerated later on.

I may want to fall back to my previous stance that COMPOSE is the better choice for block-building with needs for full vaporization. We should still be able to parameterize REDUCE for when you want handling other than erroring or turning into bad-words, as has been demonstrated. I feel like that might give flexibility without letting people fall into traps here that don't really take advantage of NULL's potential for noticing use of things you didn't quite mean to use.

BAD-WORD! and its "isotopes" have introduced a kind of renaissance in how what was "void" is now being thought of.

One concept is the interchangeability of the ~void~ isotope with vanishing. This is distinct from the intent behind the ~none~ isotope, or the ~null~ isotope.

Due to this paradigm shift, I believe that the right answer for functions like DO of things that may be invisible is not to have some kind of DO/VANISHABLE, but rather to say that it is up to the caller to decide if the return of a ~void~ isotope should be vanished by a generic operator like DEVOID.

>> 1020 do []
== ~void~ (isotope)

>> 1020 devoid do []
== 1020

This is the power of having named entities to represent a value-based surrogate for intent. These entities have a form that is safe to use in blocks (their normal form) and a form that can only exist in variables (the isotope form). Quoting discerns them in the evaluator:

>> var: '~void~
== ~void~

>> var: ~void~
== ~void~ (isotope)

>> second [var: ~void~]
== ~void~

Writing sound code based on these ideas is a new art form, and I am sure there is a lot to explore with this new design. You have to become familiar with the ^ operator and how it goes "meta" on what you are working with, to move it into the concrete domain, then shift it back into the meta meanings again.

It's already showing its mettle in UPARSE, which is proving ground for how someone can write something sophisticated and composable without resorting to C.

Executive Summary: ~void~ (isotope) is the right answer for do []. I don't know yet what REDUCE's disposition should be, as it is making a BLOCK! it does not have the choice to preserve the isotope status of a ~void~ so it would be put in the result block as a non-isotope...or it could vanish.

So questions are still open, but I do think that NULL is now ruled out as the result of do []

2 Likes