Why IF is void on no branch, while CASE+SWITCH are null

For a time, all control structures returned void when they did not branch. This was motivated in particular by wanting IFs to vanish quietly when they didn't take their branch. At first it was just COMPOSE:

 >> compose [<a> (if false [<b>]) <c>]
 == [<a> <b>]

But this spread to things like REDUCE, ANY, ALL, etc.

>> reduce [1 + 2 if false [30] 100 + 200]
== [3 300]

>> all [<a> if false [<b>] if true [<c>]]
== <c>

The cleanliness of this is extremely desirable. It would lose a lot if you had to throw in a NULL-TO-VOID conversion (what we call "MAYBE")

>> compose [<a> (maybe if false [<b>]) <c>]
== [<a> <b>]

>> reduce [1 + 2 maybe if false [30] 100 + 200]
== [3 300]

>> all [<a> maybe if false [<b>] maybe if true [<c>]]
== <c>

VOID Becomes A Casualty Of Its Friendliness

At first, void variables would cause errors on access. But this was softened, allowing them to be fetched without using GET-WORD! access or other fanfare:

>> x: void

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

Having things like CASE and SWITCH produce these is easily is a little bit unsettling. All that has to happen is that you have a branch list that is not exhaustive.

For instance:

lib: switch config.platform [
    'Windows [%windows.lib]
    'Linux [%linux.a]
]

You've now got LIB as VOID. It's uncomfortable to think about how such casual creation of voids makes a value that will go around opting out of things.

This puts a burden on people writing such lists to throw in a FAIL at the ending:

lib: switch config.platform [
    'Windows [%windows.lib]
    'Linux [%linux.a]
    fail  ; not so hard to do this if you don't want VOIDs
]

But...What if you WANT a NULL?

As it happens, due to the SWITCH and CASE "Fallout" Feature, this also works with things like NULL:

lib: switch config.platform [
    'Windows [%windows.lib]
    'Linux [%linux.a]
    null
]

Basically if you give an evaluative clause with no branch, the clause drops out if it is reached.

It is weird, but it's foreignness doesn't necessarily make it bad. Though were it a CASE statement, some people might gravitate toward an always-TRUE branch as not violating the structure:

lib: case [
    config.platform = 'Windows [%windows.lib]
    config.platform = 'Linux [%linux.a]
    true [null]
]

But SWITCH has no equivalent, so its either the fallout feature or an ELSE (which won't please @rgchris and I think I prefer fallout)

lib: switch config.platform [
    'Windows [%windows.lib]
    'Linux [%linux.a]
] else [null]

You'd Need A VOID-TO-NULL Operator :thinking:

You might think that TRY could nullify-voids, but this creates conflation of disabling raised errors returned by branches. e.g. what if the Windows branch tried to READ a nonexistent file?

lib: try switch config.platform [
    'Windows [read %windowslibname.txt]  ; imagine raises error
    'Linux [%linux.a]
]

Not being able to open the file would conflate with not taking a branch--no good. This shows the use of TRY to convert voids to nulls is clearly a poor idea, and that needs to be a special purpose function.

So TRY is off the table at this point; it's an ignore-raised-error-and-continue.

DEVOID is a cryptic name that was actually proposed to turn VOIDs to NIHILs for vaporization in situations that didn't naturally vaporize voids:

>> 1 + 2 void
== ~void~  ; anti

>> 1 + 2 devoid void
== 3

So I will call this something like NULLIFY-IF-VOID until a better idea comes along. Clearly not great for everyday use.

Ultimately, NULL For Non-IF Control Structures Was Chosen

IFs can be as light as if condition '10 It seems VOID is a relatively safe answer for IF, because it has one branch and you're very aware that it takes its branch or it doesn't.

But if you're writing a CASE or a SWITCH it's probably not tiny, and saying MAYBE CASE or MAYBE SWITCH isn't much of a burden.

So twistier constructs like SWITCH and CASE are more conservative and evaluate to NULL when a branch isn't taken.

(We might decide that the empty cases case [] and switch [] are VOID, and then you have a bit more data of when no conditions were run. Is that useful?)

Some Casualties Of Equivalence

In a perfect world, I would like to be able to write something like:

 unmeta* any [
     meta* if condition1 [branch1]
     meta* if condition2 [branch2]
 ]

...and have it be the same as:

case [
   condition1 [branch1]
   condition2 [branch2]
]

Transformations like this which relate constructs together means people can build on reliable parts. The response of ANY when all expressions void out is to give a VOID, and that's by design.

But I don't know what part of the universe breaks if ANY on a bunch of IFs isn't the exact same as CASE. It's the same if you pipe pure null to pure void.

2 Likes