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 OPT 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 OPT in a chain as you felt was warranted, if you expected any steps could fail. So perhaps first opt select block opt second options. A reader could tell which operations could potentially fail using this method.

It has shown systemic success:

>> case [null [[a [b] c]]]
== \~null~\  ; antiform

>> second case [null [[a [b] c]]]
** Error: SECOND doesn't take NULL as input (use OPT to opt-out)

>> opt case [null [[a [b] c]]]
== \~[]~\  ; antiform "void"

>> second opt case [null [[a [b] c]]]
== \~null~\  ; antiform

>> first second opt case [null [[a [b] c]]]
** Error: FIRST doesn't take NULL as input (use OPT to opt-out)

>> first opt second opt case [null [[a [b] c]]]
== \~null~\  ; antiform

>> first opt second opt case [okay [[a [b] c]]]
== b

VOID is a Better Choice Than NULL, SPACE, etc.

Prior to the invention of void, the strategy was tried as "SPACE-in-NULL-out". But this created problems since sometimes SPACE 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 OPT.

  • 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? opt some-dir: get-env 'SOME-DIRECTORY [
     ...
]

If GET-ENV returns null, the OPT 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:

1 Like