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.
As another tradeoff that I think is worth it, I've changed COLLECT to return NULL if there are no non-null KEEPs.
>> collect []
// null
>> collect [keep case [1 > 2 [<nope>] 3 > 4 [<also nope>]]
// null
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, there's an easy workaround for if you want to ensure you always get an empty block back on no keeps... just do a plain non-/ONLY KEEP of an empty block. Splicing nothing (vs null) is counted as enough intent to get some kind of result:
>> maybe-empty: []
...
>> collect [keep [] for-each x maybe-empty [keep x]]
== []
We 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 []]
]
]
But if you ask me, the idiom of keep [] isn't too bad. Moreover, if you just use data: try collect [...] you'll get a BLANK!, and blank is good enough to opt out of many operations that a []
would be opting out of:
data: try collect [if false [keep <not kept>]]
for-each x data [print "won't run, data is blank and opts-out of FOR-EACH"]
Plus you might prefer to have something that you can test for absence with conditionality, vs EMPTY?. (if empty? data
is longer than just if data
)
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 BLANK! to opt out, TEXT!, or BLOCK! to be SPACED. So it will error and force you to make an explicit choice:
print try collect [...] ;-- no output if there are no non-null KEEPs
print collect [keep [] ...] ;-- blank newline if no more KEEPs come along
print collect-block [...] ;-- blank newline if no KEEPs, if we put it in the box
So this keeps you paying attention.
But that's just a bonus, the real feature is tighter expression
A lot of code already can't have a non-empty collect because it has literal KEEPs that always run, or collections that aren't ever empty. Among the cases that are left, many want to do something different on an empty case and can do so with ELSE. Among the cases that are left after that, many just want to enumerate the result...and a BLANK! from try collect serves better than the empty block does.