The VOID-in-NULL-out Protocol

In traditional Redbol, if you wrote a random chain like like first select block second options, there was the question of how you would manage the situation of any of these things failing.

People would request lenience... to say more operations should assume that if they got a NONE! input that they should just return a NONE! output. But this would give those chains no error locality... you'd get a NONE! at the output and not know what failed. The FIRST? The SELECT? The SECOND...?

You can see DocKimbel's response to a request that INDEX? NONE be NONE:

meijeru
"There are precedents for built-in functions on series which yield none for none."

DocKimbel
"Yes, but the less of them we have, the better, as they lower the robustness of user code, by making some error cases passing silently. The goal of such none-transparency is to be able to chain calls and do nothing in case of none, avoiding an extra either construct. In the above case, it still requires an extra conditional construct (any), so that is not the same use-case as for other none-transparent functions (like remove)."

But I Didn't Give Up So Easily...

The strategy cooked up for Ren-C is called "VOID-in-NULL-out".

There is an asymmetry created, in which certain functions take VOID as input, but then just return NULL out.

Then MAYBE can turn nulls into VOID.

Since (nearly) no function naturally takes NULL as an input, this creates a dynamic where you'd put as many MAYBE in a chain as you felt was warranted, if you expected any steps could fail. So perhaps first maybe select block maybe second options. A reader could tell which operations could potentially fail using this method.

It has shown systemic success:

>> case [false [[a b c]]]

>> second case [false [[a b c]]]  ; "opt out" gives you a first step w/o error
== ~null~  ; isotope

>> first second case [false [[a b c]]]
** Error: FIRST doesn't take NULL as input (use MAYBE to opt out via VOID)

>> first maybe second case [false [[a b c]]]
== ~null~  ; isotope

>> first second case [true [[a [b] c]]]
== b

VOID is a Better Choice Than NULL, BLANK!, etc.

Prior to the invention of void, the strategy was tried as "BLANK!-in-NULL-out". But this created problems since sometimes blank was meaningful as input.

VOID has all the properties we're looking for, here. It's basically about as out-of-band as you can get.

  • Like NULL, it can't be stored in blocks

  • There's a nice operator for turning NULL into VOID and pass everything else through called MAYBE.

  • The majority of routines don't have natural meanings for void arguments, so "no op" is usually the best reaction to "hey, you got a void".

    • There are exceptions, e.g. functions like REPLACE which receive voids can interpret them as a desire to use an empty replacement vs. a no-op:

      >> replace [a b c] 'b void
      == [a c]
      
      >> replace "abc" "b" void
      == "ac"
      
  • Void has the appealing character of being able to polymorphically mean nothing regardless of what it's substituting in, so you don't have to switch between spread [] and "" when working on blocks vs. strings.

1 Like

Note that VOID-in-NULL-out can't be followed blindly. As an obvious-if-you-think-about-it case, LOGIC!-returning routines have to handle void a different way.

Getting Tricked By Inverse LOGIC!

I wanted to write the following;

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

If GET-ENV returns null, the MAYBE turns it to void for EXISTS? to process. But...what if the routine were called DOESN'T-EXIST?, and it followed VOID-in, NULL-out? It would make it look like void 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.)

1 Like