BLANK!-in, NULL-out convention vs. LOGIC!-returning actions


#1

We’re now in a situation where NULL is rejected by most routines as input. Notable exceptions being the thing-to-APPEND, or the thing-to-KEEP…anything where a BLANK! acts legally as a “thing” you might want to be talking about–and NULL is the only way to opt out.

In this world, “BLANK!-in, NULL out” has been showing off excellently as the replacement for what was long discussed under the title none propagation. I summarize the theory of the benefits here, and why routines taking “NONE!-in, NONE!-out” were a dangerous idea.

But it can’t be followed blindly. Just as append copy [] _ returns [_] and not NULL, some judgment has to be applied. As an obvious-if-you-think-about-it case, LOGIC!-returning routines have to handle blanks a different way.

Getting Tricked By Inverse LOGIC!

I wanted to write the following;

if exists? try some-dir: get-env 'SOME-DIRECTORY [
     ...
]

If GET-ENV returns null, the TRY blanks it for EXISTS? to process. But…what if the routine were called DOESN’T-EXIST?, and it followed BLANK!-in, NULL-out? It would make it look like blank inputs did exist, if you were just checking the result for truthiness or falseyness. :frowning:

This seems like a pretty solid proof that functions returning LOGIC! should not conflate their answers with NULL. (Note: I know exists? is currently conflated with FILETYPE OF, so it doesn’t actually return a LOGIC!, but that’s just a bug that hasn’t been tended to. The point stands.)

But what what about NaN handling?

I had a theory that BLANK!s and NULLs could act as the quiet NaN and signaling NaN forms of “not-a-number” (NaN). The goal of this is to allow math handling to be more graceful, without needing to set up TRAPs and such–you can be selective about which operations you are willing to have fail, and supply code to fill in such cases.

Wikipedia has a little table about how NaNs work with comparisons:

Comparison between NaN and any floating-point value x (including NaN and ±∞)

  • NaN ≥ x => Always False
  • NaN ≤ x => Always False
  • NaN > x => Always False
  • NaN < x => Always False
  • NaN = x => Always False
  • NaN ≠ x => Always True

Look at that last case. If BLANK! is the quiet NaN, you can’t have that comparison returning NULL, because it would be falsey instead of truthy.

When these routines get BLANK! they have to decide whether to return true or false depending. It’s a close analogy to how “exists?” and “doesn’t-exist?” must use their discretion on blank input.

However, the math operations that normally return numbers, and feed into these situations DO follow blank-in-null out. This is the proposed behavior:

>> square-root -1 // Note: `square-root _` is also null
// null

>> try square-root -1
== _

>> 1 + square-root -1
** Error: + doesn't accept NULL for its value2 argument

>> 1 + (square-root -1 else [10]) // selective handling
== 11

>> 1 + try square-root -1 // propagation
// null

>> 10 != (1 + try square-root -1)
** Error: != doesn't accept NULL for its value2 argument

>> 10 != (try 1 + try square-root -1)
== #[true]

So that demonstrates a bit of nuance involved in the “BLANK!-in, null out” rule. LOGIC!-bearing routines should still only return LOGIC!, and if for some reason they can’t make a reasonable call one way or another, they need to error vs. ever returning NULL.