Meet VETO: The VETO-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 "VETO-in-NULL-out".

There is an asymmetry created, in which if a function receives the "hot potato" unstable antiform ~(veto)~, then it will return NULL after it has typechecked its other arguments:

>> foo: lambda [x [integer!] y [integer!]] [x + y]

>> foo 1000 20
== 1020

>> foo ~(veto)~ 20
== \~null~\  ; antiform

>> foo ~(veto)~ "not an integer"
** PANIC: foo got ~{text!}~ for y argument, expected [integer!]

Then COND can turn nulls into VETO for you, passing through other values

>> num: 1000

>> foo (cond num) 20 
== 1020

>> num: null

>> foo (cond num) 20 
== \~null~\  ; antiform

Since few functions naturally takes NULL as an input, this creates a dynamic where you'd put as many COND in a chain as you felt was warranted, if you expected any steps could fail. So perhaps first cond select block cond 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]]]
 ** PANIC: second got ~null~ for value argument, expected [any-series?]

>> cond case [null [[a [b] c]]]
== \~(veto)~\  ; antiform (pack!)

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

>> first second cond case [null [[a [b] c]]]
 ** PANIC: first got ~null~ for value argument, expected [any-series?]

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

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

If You Actually Want ~(veto)~ Args, Use [<veto>]

While the default behavior for receiving veto is to bypass the function and return null, a very small minority of functions actually want to receive ~(veto)~ antiforms as an argument.

One obvious case of this would be the VETO? function for testing if something is a veto. But also, PACK? wants to report that it is--indeed--a PACK!. The TRY function defuses vetoes at the moment, and so it's another case that inspects it.

To receive it, mark your argument with [<veto>].

>> vtest: proc [^arg [<veto> any-value?]] [
       if veto? ^arg [print "I got a veto!"]
   ]

>> vtest veto
I got a veto!

VETO (and COND) Can Be Used Lots of Places!

You'll find that VETO-awareness comes up many places, to let you short-circuit an operation.

For instance, REDUCE:

>> reduce [1000 + 20, if 1 < 2 [veto], 300 + 4]
== \~null~\  ; antiform

>> item: <thing>

>> reduce [1000 + 20, cond item, 300 + 4]
== [1020 <thing> 304]

>> item: null

>> reduce [1000 + 20, cond item, 300 + 4]
== \~null~\  ; antiform

You can similarly opt out of slots in COMPOSE:

>> compose [1020 (veto) 304]
== \~null~\  ; antiform

But It's Very Powerful In PARSE!

While the results of a GROUP! are typically ignored after being evaluated in PARSE, a VETO is special... so if a group evaluates to a VETO it goes to the next match rule!

>> parse "aaa" [(if 1 = 1 [veto]) some "a" | some "a" (print "hello")]
hello
== "a" 

This gives you great flexibility for sending the PARSE a signal from inside the group, without using the INLINE combinator. Having the GROUP! decide "no, I don't want to match, skip to the next parse alternative" is probably the most common signal a group would want to send!

2 Likes

Note that the semantics of VETO-in-NULL-out should be considered carefully. As an obvious-if-you-think-about-it case, LOGIC!-returning routines have some conflation.

Getting Tricked By Inverse LOGIC!

I wanted to write the following;

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

If GET-ENV returns null, the COND turns it to ~(veto)~ for EXISTS? to process and return null.

Great, that's what I wanted. But...

what if the routine were called DOESN'T-EXIST?, and I used VETO-in, NULL-out? It would make it look like vetoed inputs did exist, if you were just checking the result for truthiness or falseyness.

:thinking:

So be careful with that. It does what it does--you can predict it. But make sure you don't start assuming things like "NULL coming back from EVEN? means what was passed in was odd", when you start throwing VETO tricks into the mix.

1 Like

It was a Long Crawl to VETO-in-NULL-out

:snail: :atom_symbol:

Circa 2018, Ren-C had just one "antiform" (what we might retroactively call voidnull).

It didn't have any unstable antiforms... much less undecayable unstable antiforms!

So the first attempts were simply "BLANK-in-VOIDNULL-out". And OPT was used to convert "voidnulls" to blanks.

>> first _
; voidnull

>> first first _
** PANIC

>> opt first _
== _

>> first opt first _
; voidnull

Of course, this ran up against some difficulties with routines that had meaning for blanks.

>> append [a b c] _
== [a b c _]

>> append [a b c] opt second [d]
== [a b c _]  ; wanted [a b c]

So when void and null became split into two distinct states, it switched to VOID-in-NULL-out:

>> ()
; void

>> first ()
; null

>> first first ()
** PANIC

>> opt first ()
; void

>> first opt first ()
; null

That made more sense. Yet VOID-in-NULL-out still competed with a desirable meaning for void in many places, such as APPEND's desire to treat void appends as a no-op that returned the original series:

; desire #1

  >> append [a b c] opt second [d]
  == [a b c]

; desire #2

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

For a short while, the idea of using antiform splices as the result of OPT was tried instead of having it return void. The COND operator was then shifted to produce VOID. This allowed VOID-in-NULL-out to coexist with NONE-in-NOOP-out:

; !!! Slightly misguided OPT change that lasted about a week !!!

>> opt null
== \~[]~\  ; antiform (splice!) "none"

>> cond null
== \~,~\  ; antiform (ghost!) "void"

>> append [a b c] opt null
== [a b c]

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

This was the right split of intent between OPT and COND, but the wrong states to do it with.

So when the ~(veto)~ undecayable antiform state was discovered, COND could return VETO, with a universal default behavior of arguments short-circuiting a function call when ~(veto)~ was received.

OPT was reverted to its natural definition of producing voids from nulls... and became a frequent synonym for the behavior of empty splice inputs.

That's how VETO-in-NULL-out came to be.

A Time Machine With This Thread Would Have Helped

I sometimes say that isotopes started with "heavy null".

But realizing that voidnull was the "first antiform" really does drive it home that antiforms had already begun before that. The story of Ren-C is the story of isotopes.

All this seems "obvious" in retrospect. But crawling along the implementation path, it just wasn't obvious before the maturation of the isotopic model.