Long, long ago there was a datatype called NONE. In historical Redbol, it had the bad habit of looking like a WORD!:
rebol2>> 'none == none rebol2>> none == none ; same in R3-Alpha and Red
But it wasn't a word:
rebol2>> type? 'none == word! rebol2>> type? none == none!
It was a distinct type, which also happened to be falsey (while WORD!s are truthy):
rebol2>> if 'none [print "Truthy word!"] Truthy word! rebol2>> if none [print "Falsey none!"] == none
And as we can see, NONE!s served purposes of signaling "soft failures": branches that didn't run, or FINDs that didn't find, or SELECTs that didn't select... etc.
rebol2>> find "abcd" "z" == none rebol2>> select [a 10 b 20] 'c == none
Ren-C Divided NONE!s roles across NULL, VOID, and BLANK!
NULL - an "isotopic" state of WORD! that couldn't be put in BLOCK!s. Anywhere that NONE! would be used to signal a soft failure operation--like FIND or SELECT--would use ~null~.
>> null == ~null~ ; isotope >> find "abcd" "z" == ~null~ ; isotope >> select [a 10 b 20] 'c == ~null~ ; isotope >> append [a b c] null ** Error: APPEND doesn't allow ~null~ isotope
VOID - the result of things that are effectively "no ops". Unlike nulls, they will vanish in-between expressions, and when functions like APPEND get them as an argument they are treated as no-ops:
>> void ; void >> if null [print "Doesn't print as NULL is falsey"] ; void >> 1 + 2 if null [print "Voids disappear..."] == 3 >> append [a b c] void == [a b c]
(At one time void was also the state of unset variables, but that is now the isotopic state of void...currently called "nihil")
BLANK! was represented by a lone underscore (
_) and could be put into blocks:
>> append [a b c] _ == [a b c _]
It retained the choice to be falsey:
>> if _ [print "Won't print because blanks are falsey"]
Question One: Could BLANK! Just Be A WORD! ?
You might wonder if you could just say:
>> _: '_ == _
This would give you BLANK! as a WORD! that had the behavior of reducing to itself.
>> reduce [_ 1 + 2 _] == [_ 3 _]
That could be just a default, and you could redefine it to anything you wanted. Generally speaking, people do like being able to define words as operators... and _ has historically been a WORD! (Ren-C allows you to use underscores internally to words, so it feels a little bad to take away one word).
But outside of being hardcoded as falsey, what makes BLANK! fairly "built in" is that in the path mechanics, it fills in the empty slots:
>> to path! [_ a] == /a >> as block! 'a//b//c == [a _ b _ c]
Alternately, we could accomplish a "reified nothing" with a quoted null:
>> to path! [' a] == /a >> as block! 'a//b//c == [a ' b ' c]
But there's other places the blank is used, such as to opt-out of multi-returns.
>> [_ pos]: transcode "abc def" ; void >> pos == " def"
So freeing it up to be an arbitrary variable feels kind of wrong, as if it were taken for dialects like multi-return you'd be unable to set it as a variable.
This may be an argument for using something like a TAG! instead, so you're not worrying about overlapping with user variables:
[<_> pos]: transcode "abc def"
Similar arguments have led me to contemplate the dangers of using things like
[a b ...]: in case someone has assigned a meaning to the ellipsis. That might be a good reason to keep
... as a TUPLE! instead of a WORD! exception, because no one could assign it.
I'm pretty sure we should keep _ reserved as a BLANK! datatype, not a WORD!. People can still give it arbitrary meanings in dialects, they just can't assign values to it as a variable... and they can't do that with
# either or
<a> so I can live with it. Taking it away from the word pool does more good than harm.
Question Two: Does BLANK! Still Need To Be Falsey?
My feeling is that having blank be falsey doesn't have all that much benefit. NULL does a better job of it, and really what it does is mess with its usefulness as a placeholder:
>> append [a b c] all [1 < 2, 3 < 4, _] == [a b c _] ; would seem nice, but gives error today since ALL is NULL
Thinking of BLANK! as being "null-like" in terms of non-valuedness is generally a hassle. It makes you wonder about whether something like DEFAULT should think of it as being assigned or not:
>> item: _ >> item: default [1 + 2] == ???
In practice, I prefer the truly non-valued NULL being the only cases that DEFAULT overwrites. This is because NULL is far more useful than BLANK! when it comes to representing something that you think of as "not being assigned"... as you'll get errors when you try to use it places (e.g. in APPEND). Trying to use it to represent nothingness invariably leads to stray appearances in blocks (Shixin wrote a lot of code to try to filter them out in Rebmake, prior to it being switched to NULLs)
Also, the asymmetry between BLANK! and NULL were part of a scheme to try and solve what Redbols called "NONE! propagation":
>> second null ** Error: SECOND doesn't take NULL >> try null == _ >> second try null == null
We still want this general concept, but the new idea is that it's VOID which opts out cleanly from these operations, and MAYBE is the operator that produces them.
This makes more sense, and I think it bolsters the argument that BLANK! is less of a falsey-NULL relative...but more of a placeholder value. I've said "blanks are to blocks what space is to strings". And space is truthy:
>> if second "a b" [print "Space is truthy"] Space is truthy >> if second [a _ b] [print "So why shouldn't blank be truthy?"] ???
So I Suggest The Removal of BLANK! From Being Falsey. This creates some incompatibility in Redbol (which has been using NONE! as a blank substitute). But it's something that can be worked around.