Do not COLLECT [keep if false [$100]]

As I showed in the solution to FizzBuzz, being able to take advantage of the evaluator's unique chaining abilities and "opting out" generally means making clever uses of null.

I'm experimenting with COLLECT to returning NULL if there are no non-void KEEPs.

>> collect []
== ~null~  ; isotope

>> collect [keep case [1 > 2 [<nope>] 3 > 4 [<also nope>]]
== ~null~  ; isotope

When I look at that, it seems pretty natural. In contrast, giving back a block when there's been no KEEPs seems like you're fabricating something from nothing. I'll also mention that it helps some with performance/overhead, because you're not making empty blocks you don't need if you don't actually wind up needing one. (The implementation of collect in historical Rebol and Red does make block! 16, so you're taking a 16 cell block even if you don't use it, while this creates the block on demand.)

However, if this seems inconvenient, you can easily chain it to make an "always-returns-a-block" version, and maybe we should put that in the box vs. making people use that idiom:

 collect-block: cascade [
     collect/
     x -> [x else [copy []]]
 ]

The semi-noisy nature of null has advantages

If you think casual uses of COLLECT are sure they mean they want an empty block on no KEEPs, I don't know if that seems to be the case.

Consider something like "print collect [...]", with that collection coming up empty. What's PRINT supposed to be doing? Is it a request to print a blank line--just a line feed? Or is it a request to opt-out of printing altogether, so no newline at all?

I don't think there's a generic answer to that question. So it's handy to draw attention to the ambiguity, since PRINT doesn't take NULL... only VOID to opt out, TEXT!, or BLOCK! to be SPACED. So it will error and force you to make an explicit choice:

 print opt collect [...]  ; no output if there are no non-null KEEPs 
 print collect-block [...]  ; just a newline if no KEEPs, if we put it in the box

So this keeps you paying attention.

1 Like

I like how this feature and similar ones maximize robustness of the code (through handling null, blank, etc) while keeping everything extremely tight. There’s a nice balance of robustness paired with a code-golf mentality.

The libRebol API benefits in particular from NULL being in more places as a way of gleaning information without additional calls. It's even more of an advantage than in the interpreter where everything is handled automatically--because there's no handle to release, which is a separate API you have to worry about. And null pointers are conditionally falsey in C:

REBVAL* block = rebValue("collect [...]");
if (! block) {
    // stuff to handle case of nothing collected
    return;
}
// other stuff

Without using null, and having to test for empty, it's longer and easier to get wrong:

REBVAL* block = rebValue("collect [...]");
if (rebDid("empty? block")) {
    rebRelease(block);
    // stuff to handle case of nothing collected
    return;
 }
 // other stuff
1 Like

I think of COLLECT as somewhat related to REDUCE and COMPOSE which always return blocks unless there's an error.

1 Like

7 years ago I decided to make COLLECT* a lower level function that produced a NULL when there were no keeps.

Today I ran across some code that made me think about how things have changed--and whether it affects the question. One thing is dialected function calls, which could very succinctly let you say what kind of container you wanted for your collect:

>> collect:[] [keep 'a keep 'b keep 'c]
== [a b c]

>> collect:(()) [keep 'a keep 'b keep 'c]
== ((a b c))

>> collect:{} [keep 'a keep 'b keep 'c]
== {a b c}

I had the weird thought that if you specify an envelopment in that way, perhaps that could also shift it into the "don't ever make nulls" mode by default. That may be technically orthogonal, but you could still have another refinement if you wanted something like a FENCE! but still wanted it to be null in the case of no KEEPs.

Anyway, "naturally nulling COLLECT" vs. "non-nulling COLLECT:[]" has more of a purposefulness to it than the crappy collect vs. collect* situation. The decoration bias is inverted, but the decoration gets you feature exposure of choosing the container....which is pretty awesome.

I think it's awesome enough to tip the scales here. @rgchris ?

Recall it's not just about being able to easily test (with IF or ELSE) for no keeps. The safety-by-default nature of NULL-if-no-collects is worth reiterating, because people really do write things like print collect [...] without fully thinking through if a case of no collects should be no output vs. a blank line. NULL forces a decision point in the no collect case which is easy to overlook. A lot of instances are like this.

Erroring on null would force you to consciously resolve it, e.g.:

print opt collect [...]  ; no output if no keeps

print collect:[] [...]  ; empty line if no keeps

print collect [...]  ; leave alert if you fixed an accidental case of no keeps

I had another observation about something that's different today, just in terms of how NULL and VOID orchestration has gotten fully pervasive. So things like FOR-EACH can be opted out of, smoothly giving you no enumerations... as an empty block would.

But in thinking about the substitutability of voids for an empty block, I did notice a line of distinction.

If you iterate over an empty block, you get a void:

>> var: []

>> for-each 'x [] [print "BOO"]
== \~[]~\  ; antiform (pack!) "void"

But if you pass a void in as the thing to iterate, you get NULL out (due to "VOID-in, NULL-out" mechanics):

>> var: null

>> for-each 'x opt var [print "never runs"]
== \~null~\  ; antiform

This means that to an outside observer, passing in a void looks like the loop encountered a BREAK ("light" nulls are exclusively used for breaks... and apparently, void input as well).

I question this being what VOID does when used in the thing-to-be-iterated slot. VOID-in-NULL-out is a heuristic, it's not always the case:

>> var: null

 >> append [a b c] opt var
 == [a b c]  ; not ~null~

In a sense, opting out of the data to iterate is a lot more like opting out of the value to append... vs. requesting a BREAK of the loop as if it hit some kind of problem.

So... Dialected COMPOSE, Nulling non-Dialected Default?

Who's with me? Show of hands?

:hand_with_fingers_splayed:

2 Likes