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: chain [
     :collect
        |
     func [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 maybe collect [...]  ; no output if there are no non-null KEEPs 
 print collect-block [...]  ; blank 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 = rebRun("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 = rebRun("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