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