UPDATE 2022: This post contains some reasoning circa 2019 that helped lead to the ultimate design of isotopes, as a broad and nuanced replacement for the "ornery" type called "VOID!". Like NULL, isotopes cannot be put in blocks...they come in many flavors and users can name new ones, all of which have a "meta" form as a BAD-WORD!. What wound up making it all work is to say that function arguments cannot take isotopes as arguments unless they use a fundamentally different parameter convention, which quotes most values (including ordinary BAD-WORD!) but transforms isotopes into their corresponding plain BAD-WORD! form...and leaves NULL alone. To understand how this development fundamentally shifts the landscape requires looking at the examples it enables...but suffice to say, the state of an unset variable is an ~unset~ isotope and as per Carl's reasoning very few parameters--only those that are ^META convention--can receive such isotopes.
I found a post from Carl titled "UNSET! is not first class".
It's important to understand the unset! datatype; otherwise, we run the risk of assuming that it is first class (assignable, passable, returnable) when it's really not intended for that kind of usage!
He gets to the idea that it's not a "normal" value. While not taking the step to make it illegal to put in blocks, I think that was just a matter of not having thought through how to prevent it. You see the notion of a wish to quarantine something that is a "necessary evil".
Tuning the model in Ren-C, the roles of the two cases of
NONE!/UNSET! were reshaped to into NULL, VOID!, and BLANK!. This has been very successful, putting the "hot potato" nature of null to good use by keeping it something you cannot assign...while allowing it to be conditionally false. The "neither true-nor-false" role then falls to VOID!, as a prickly value that is nonetheless a value and can be put in blocks if you insist.
Should NULL assigns via SET-WORD! unset variables?
When NULL was first being introduced, it wasn't the failure result from ANY or FIND. Those still used blank, which was more convenient since that era's null was neither true nor false (like UNSET! had been).
Instead, null was sneaking in as the outcome of failed conditionals...as well as trying to be a "more correct" answer to things like select [a 10 b 20] 'c. There, a null result distinguished from a literal blank in a block, such as select [a 10 b 20 c _] 'c.
But with null being such a hot potato, there were difficulties. So it was tried that foo: null would unset the
foo variable, vs. be an error. This made it a bit less awkward to work with if you wanted to write something like:
if not null? x: select block value [ ...do stuff with x... ] else [ ...x is unset, maybe do error handling here... ]
If SET-WORD!s caused errors, that would be more tricky--you'd have to test the result of SELECT for null, but the test would return LOGIC! and lose your result for assigning.
But, enfix to the rescue...once operations like ELSE and THEN came on the scene, they offered a new possibility...instead of needing IF and a test for a branch, the branch could react to the nullness before the assignment. Whether you needed error handling or default values, this pattern addressed most needs.
x: select block value else [<default-value>]
Then null changed to be conditionally false, with VOID! picking up its neither-true-nor-false duties. Routines like ANY and ALL and FIND began returning NULL on failure, which could be used in conditionals without any extra work. Then conversion of nulls to blanks was made as easy as the short, repurposed word TRY.
With this change, it was moved back to where null assignments to SET-WORD! were errors, though I've sometimes wondered if there would be an advantage to letting NULL implicitly unset variables. Should you be able to say pos: find block value and then if set? 'pos [...] or do you really need to say pos: try find block value?