Why VOID! is not like UNSET! (and why it's "more ornery")


#1

Today’s #[void] has a lot in common with R3-Alpha’s #[unset].

  • It’s what functions return when they want to say they do not return “interesting” values, e.g. PRINT
  • Voids are values, and have a datatype (VOID!)
  • If you try to assign a value of type void via a SET-WORD! or SET-PATH!, you’ll get an error
  • If you try to access a variable holding a VOID! through a plain WORD! or PATH!, you’ll get an error (this is true of unset variables also, but that gives a distinct error of “unsetness”)
  • Voids are considered to be neither true nor false–giving an error if used in conditional expressions

So one might wonder:

“If VOID! is acting so much like UNSET!, is this saying that Rebol2/R3-Alpha were right all along? Did Ren-C just complicate things a bunch and then come full-circle to where all that happened is it changed the name? Why not just call it UNSET!?” — Hypothetical Person Who Is Not Paying Attention

I can’t respond to this more clearly than I already have, but I’ll say it again: Variables can be “unset”, not values. A variable holding a value that indicates the value is unset is a contradiction in terms.

So void is its “own thing”:

A void! is a means of giving a hot potato back that is a warning about something, but you don’t want to force an error “in the moment”…in case the returned information wasn’t going to be used anyway.

You can see this very clearly in how VOID! helps avoid mistakes when using conditionals with ELSE branches:

 data: if condition [expression-that-returns-null] else [value2]

ELSE is triggered by nulls. But you wouldn’t want a routine that incidentally returned nulls to wreck your logic–this should not be running the ELSE branch just because the IF branch returned null.

So nulls get “voidified”; when an if runs a branch that evaluates to null, it gives back VOID!..saving null as the signal for a branch not running. This fills in the gap, avoids running the else, then the overall IF…ELSE evaluates to VOID!. This means the assignment doesn’t work, you’ll find out. You can stick a TRY on that expression and get a blank, but at least you’re aware you won’t be returning a null (you can OPT it back to a null if you must):

 data: opt if condition [try expression-that-returns-null] else [value2]

But what if you hadn’t had an assignment? There’s no reason to error on:

 if condition [expression-that-returns-null] else [value2]

Having the truthy branch convert the NULL into a VOID! in this case allows the logic to work just fine. The void appears, does its job as a value cue, and vanishes. You didn’t find out there was a problem because there wasn’t one–you weren’t going to assign the value anyway.

But while that isn’t really about unsetness of variables, it does involve NULL, as only nulls are turned into voids. So perhaps seeing the distance from unsets can be demonstrated through another construct…

How MATCH uses VOID! to warn you of a tricky case

MATCH is a promising tool. It a takes a value and a test, and if the value passes the test it gives the value back…or gives you a null otherwise:

 >> match integer! 3
 == 3

 >> match integer! <some-tag>
 // null

That’s useful. All the more useful since you can use typesets expressed as blocks (match [integer! text!] …), logical truth or falsehood (match false blank and match false 1 = 2 being matches), or even direct function applications (match parse “aaa” [some “a”] being “aaa”)

It seems useful as a conditional, but what if you say:

 whatever: false
 ...
 if match [logic! integer!] whatever [print "It's a match!"]

If it passed through the false, then even though the MATCH was “successful”, it wouldn’t print It's a match!. This could very easily get confusing and lead to mystery bugs.

But VOID! to the rescue. By turning any falsey-valued matches to voids, you find out what’s up:

>> if match [block! blank!] 10 [print "10 isn't a block or blank"]
10 isn't a block or blank // because match returned null

>> if match [block! blank!] blank [print "conveniently warns you!"]
** Script Error: VOID! values are not conditionally true or false

You still get a distinction…the second case returns a value, the first returns a null. So you can test for valueness:

 >> if value? match [block! blank!] 10 [print "matched"]
 // null

 >> if value? match [block! blank!] blank [print "matched"]
 matched!

 >> match [block! blank!] blank then [print "matched!"]
 matched!

But if that’s not good enough, the gift to you is that you now know that MATCH is not the construct you wanted. You’ll need to use the routine match is based on, called EITHER-TEST:

 >> either-test [block! blank!] blank [print "branch if no match"]
 == _

 >> either-test [block! blank!] 10 [print "branch if no match"]
 branch if no match

Deferred errors is the essence of VOID!, not unsetness

Hopefully you can see why UNSET! is not a suitable name for the type. It’s an ornery value that your routines can use for the “hot potato” situation.

So it’s best to make voids about as irritating as possible–while still being technically able to write code that builds and inspects structures using them. Almost no one should be doing so, but they can.

In this spirit, VOID!s have been made even more ornery than R3-Alpha unsets. e.g. they cause errors in ANY and ALL vs. just being ignored. (I wrote up a pretty good explanation of how invisibles provide every real desire that was intended for…tidying up a bunch of murky issues nicely.)

Also, R3-Alpha used to be able to append/insert/modify UNSET! into blocks:

r3-alpha>> append copy [a b c] print "Hello"
== [a b c #[unset]]

But now you need /ONLY to add VOID!s to blocks in Ren-C:

>> append copy [a b c] print "Illegal now"
Illegal now
** Error: VOID! cannot be put into arrays without using /ONLY

>> append/only copy [a b c] print "Must use /ONLY"
Must use /ONLY
== [a b c #[void]]

Even Deeper: Shuffling the logic of OPT and TRY

TRY was created as a way to turn nulls into blanks. For a while it decided to turn voids into blanks too–apparently there was a situation where it seemed useful. I can’t find that situation now, and it seems like a pretty bad idea. If such an operation is ever needed, it could be called DEVOID or something. So now TRY on a void will error.

But a parallel to TRY is OPT, which changes blanks into nulls. Previously the behavior for nulls was also to produce nulls, as a no-op. But now the twist is OPT turns nulls into voids.

This stops you from having a value that you don’t know if it contains blanks or nulls as an opting out form, but you OPT it “just to be sure”. You can still do that, with opt try (expression), since TRY has to allow both blanks and nulls to become BLANK!. It’s unusual enough that you would want to do this that having the unusual pairing is probably good for communicating that you don’t really know which it is.

But it gives another cool property, which is that if you have something you know can be NULL, you can generate voids from it while passing through other values. This is helpful if you want to stop a reduce from failing.

>> reduce [quote data: select [a 10 b 20] 'c]
** Script Error: REDUCE evaluation produced a null

>> reduce [quote data: opt select [a 10 b 20] 'c]
 == [data: #[void]]

So that gives you some of the behavior of what you might have done with an unset in the past… instead of erroring in the moment, you push off the error to some future situation (which may not run at all, so it might be okay). You aren’t forced to use TRY, which might sweep the situation under the rug–it’s still a “noisy” void.

And something that voiding OPTed NULLS instantly helped with… finding superfluous opts. I found several due to the change, where it was being called when it wasn’t needed because the routine’s failure naturally returned null already. This is a holdover from when many routines would indicate “soft failure” with the none!-like BLANK!.


Name for prefix forms of ELSE and THEN... EITHER-TEST-XXX?
#2

Ornery value from an ornery fork. :wink: