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.