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

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