What to do about `do []` and `()`


#1

Rebol2 considers both do [] and () to be #[unset], and neither true nor false. But that doesn’t necessarily suggest things always run through a common path. The following situations are similar, but give different error messages:

rebol2>> if () [print "won't work"]
** Script Error: if is missing its condition argument

rebol2>> while [] [print "similar situation"]
** Script Error: Block did not return a value

Furthering the “not going through the same path” situation, Red either intentionally-or-unintentionally permits the first:

red>> if () [print "uh, it's truthy here?"]
uh, it's truthy here?

red>> while [] [print "but this is a problem for some reason"]
*** Script Error: block did not return a value

Historically in Ren-C both () and do [] were null… the absence of a value. However, absence of a value was ultimately deemed to serve best as being conditionally false, as NULL is in C. This falsey null state has became a cornerstone of the API interaction, and the out-of-band non-valued definition of “soft failure” for functions like SELECT or FIND. Which is turning out fantastic.

But when you run things through common code paths and try to get common behaviors, you get some differences. This led to while [] seeing that condition block as producing null and thus being falsey, as if you had written while [null] [...]. So there was no error. And if () [print "this just wouldn't print"]…falsey again.

Enter the void

Yet Ren-C has something that is neither true-nor-false, the #[void]. You really can think of it very much like “an unset”–except it has nothing to do with variables being unset. It is a “void value”–a legitimate item that can appear in blocks (the way #[unset] deceptively could). It’s not quite the “hot potato” represented by null, but it causes errors in conditional slots.

But when to make a void? Here are some situations to look at:

a: func [] []
b: func [] [return]
c: func [] [do []]
d: func [] [if true []]
e: func [] [()]

Should these do the same thing? Different things?

Previously, we had the situation where if b […] would fail with the argument-less RETURN yielding void, and if d […] failing because d is void (the branch ran and had to be distinguished from NULL). But the other cases would return plain old NULL.

Something about it felt wrong to me. It seemed to me that at least a/b/c/d should return the same thing, yielding a function that would error if you tried to use it in a conditional clause. And possibly/probably e too.

The impact of splicing

Imagine that you have something like block: [a b c]. There is today–and probably for the future of binding–a difference between:

code: [... do block ...]

…and:

code: compose/only [... (as group! block) ...]

They might look similar, in that they execute the block through a “link”/“reference”, without making a copy.
But the latter makes the contents of the block visible as a group, which means an operation like BIND on the code will affect it. So imagine if BLOCK contained things like RETURN in it and you tried to use this code as the body of a function. The first case would leave whatever binding was on the return initially, while the second would pick up the binding for the new function.

But let’s step away from the binding can-of-worms and focus on the question of what this might imply for our needs in the decayed case where the BLOCK! is empty. What is the likely intent?

One possibility is that composing in the as group! is semantically the same as splicing, but the goal is to avoid duplicating the block’s contents. The wish to avoid duplication might be for memory efficiency, or it might be because it’s could be modified and the modifications should be seen. If this is the case, then the behavior of () might be best if it were the same as an expression barrier.

>> 1 + () 2
** + is missing its value2 argument

>> 1 + 2 ()
== 3

That’s weird, but it sort of makes sense. It makes GROUP!s a little more ghostly, but it keeps them from coming up with values that aren’t there.

If DO [] is #[void], and () has this behavior, it makes the a/b/c/d/e examples above all return the same thing. It also makes if () [...] an error, but for a different reason…not because it’s trying to determine the conditional truth or falsehood of a void, but because the IF “hit a wall”…as if you’d written if | [] or (if) [].

Does this seem coherent?


Issues with "Invisibles": a truly disappearing COMMENT