Treat BLANK!s from Variables or Evaluation Like NULL

BLANK! has a very tempting nature to make you want to use it to mean something. For instance, I've consistently wanted it to mean space in DELIMIT's "dialect" :

write port unspaced [
    "HTTP/1.0" _ code _ code-map/:code CR LF
    "Content-type:" _ type CR LF
    "Content-length:" _ (length of body) CR LF
    CR LF
]

That is awesome. It lets you see what's going on very clearly. I'm always annoyed when people include spacing in the edges of the things being spaced, like "HTTP/1.0 " ... because it makes it harder to factor. It also makes it hard to see what's a string and what's not:

 print [" At a " glance " how " do " you tell " whats " a string "]

But as awesome as BLANK!-spacing is, it has a downside. It sucks if a variable holds BLANK! with the intent to mean nothing if that nothing becomes a space character.

As a simple example to demonstrate, let's say you've got a situation like this where the second item is supposed to be "not there":

values: ["one" _ <three>]

print "And here the values are!"
for-each item values [
    print [item]
]

Clever mechanics in PRINT and SPACED won't print lines that fully vaporize. So if that values/2 were an actual NULL, you'd get just the two lines printing...with no newline between them.

But BLANK! acts as a space. So instead, you get something that's actually worse than it looks:

And here the values are!
one

<three>

I say it's worse because that empty line isn't actually empty. It actually wrote a space and a newline.

You physically cannot put NULLs in BLOCK!s. And branches have become more involved with turning NULLs into voids to signal when a branch ran (some creative tools to deal with that coming soon). So it becomes a juggling act to switch back and forth between null and blank when you might consider it easier for your purposes to stick with blank for some things.

Why Not Try Differentiating Evaluated BLANK! vs. Literal?

>> print ["a" _ "b"]
a b

>> var: _
>> print ["a" var "b"]
ab

Well... OF COURSE that was tried. Multiple forms of PRINT and DELIMIT over time have experimented with subtly different rules to try and find a sweet spot.

But you should be careful in dialects where replacing a literal with a variable gives a different effect. In particular, this was noticed with BLOCK!s...where the behavior felt very unpredictable when an in-place block did something different from one accessed through a variable:

>> print [["doesn't" "space"] "does" "space"]
doesn'tspace does space

>> block: ["doesn't" "space"]
>> print [block "does" "space"]
doesn't space does space

Some historical examples in this vein were tried and discarded. It seemed to run against the idea of being able to abstract what you were writing.

You can think of other examples like this. What if PARSE decided that an in-place integer meant a count, but one through a word! matched the number?

>> did parse "aa" [2 "a"]
== #[true]

>> count: 2
>> did parse "aa" [count "a"]
== #[false]

>> did parse [2] [count]
== #[true]

It doesn't do "double execution" of WORD!s that contain WORD!s. But static data is substituted as-is, including BLOCK!. So what is BLANK! more like...?

I Think BLANK! is Special, and We Have QUOTED! Tools Now

The subject of this post sums up how I think the competing demands on BLANK! should be resolved:

You should treat BLANK! the same way you would treat a NULL if you fetch it from a variable or an evaluation. But if it's at "source-level" you can give it another meaning, to take advantage of its unique visual properties. If it has no meaning at source level, a literal BLANK! should raise an error.

(Here I'm going so far as to say that you nearly shouldn't let a source-level blank mean the same thing as a fetched null in your dialect. Maybe that's not necessarily a good rule...but there's something giving me an inkling that it might be.)

You might point out that the definition of "source-level" is slippery, as people can be COMPOSE-ing code together. But when you compose, you'll have issues with WORD! as well:

>> foo: void
>> var: 'foo
>> print ["a" var "b]
a foo b

>> print compose ["a" (var) "b"]
** Script Error: foo is VOID! (Note: use GET/ANY to GET voids)

But the same quoting tool that can help with this can help with BLANK!, because quoted blanks are evaluated (all the evaluation does is drop the quote, but it's still evaluation)

>> foo: void
>> var: 'foo
>> print compose ["a" '(var) "b"]
a foo b

>> print ["a" _ "b"]
a b

>> print ["a" '_ "b"]
ab

>> var: _
>> print ["a" '(var) "b"]
ab

This Feels Like Solid Guidance

I think this is pretty much as good as it gets. Like I say, it pays to be cautious when making substitutions of values behave differently than if they appeared literally...but this seems to work.

It does have some kind of strange implications for PARSE and BLANK!. We know that QUOTED! means match directly:

>> did parse [1 _ 'x] ['1 '_ ''x]
== #[true]

And if you fetch out of a variable you're supposed to treat it as if it's not there:

>> rule: _
>> did parse "abcd" ["ab" rule "cd"]
== #[true]

So would it be acceptable to let the literal form mean "skip"?

>> did parse [1 <foo> "hi"] [integer! _ text!]
== #[true]

I think that's within the rules, and can be a design choice. The key is just that when it comes from a variable, it has to act the same as null. What you do in your dialect with quoted forms that are literal is as much up to you as what you do with the plain forms.

Hopefully this resolves a couple of years-old questions:

>> rule: _
== _

>> parse [a b _ _ _] ['a 'b 3 rule]
== [_ _ _]  ; residual

>> rule: quote _
== '_

>> parse [a b _ _ _] ['a 'b 3 rule]
== []  ; all parsed

What's nice is that QUOTE gives you a generic way to turn a value you want to match into a value you know will work in the parse. Without generic quoting you wouldn't have it! It's a little bit easier to look at than:

>> rule: ''_
== '_
1 Like