MAYBE IF conditionals went back to returning NULL on Failure?

A while ago, I made void more friendly... to say that void variables could be fetched without using GET-WORD! access or other fanfare:

>> x: void

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

My rationale was something along the lines of "well, if you make a void variable, then you get what you were asking for".

A little bit unsettling is how easy you can make voids if something like your branches in a CASE or SWITCH are not exhaustive. For instance:

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

You've now got LIB as VOID. It's a little uncomfortable for me to think about how such casual creation of voids makes a thing that will go around opting out of things, but those opt-outs produce NULL via the void-in-null-out protocol, so you should find out about it eventually. My other argument was that if you didn't want this, then you should just 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]

Or Is This A Job For A VOID-TO-NULL Operator?

I thought maybe 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.

MAYBE makes more sense as NULL-TO-VOID and serves well in that purpose.

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

>> 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.

Or... Is This Cold Feet on Void From Failed Conditionals?

Production of voids from failed conditionals was motivated almost entirely by wanting IFs to vanish quietly when they don'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>

In terms of composability and convenience, it's hard for me to think this isn't the right answer. It feels like they get mutilated if you have to throw in a NULL-TO-VOID (what I've called "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>

But perhaps it's only a good answer for IF, because it has one branch and you're very aware that it takes its branch or it doesn't? Maybe twistier constructs like SWITCH and CASE should be more conservative and evaluate to NULL when a branch isn't taken...? :face_with_raised_eyebrow:

I don't like it for a couple of reasons, but a big one is that it feels like it throws a wrench into equivalencies. e.g. I'd like:

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

...to be the same as:

case [
   condition1 [branch1]
   condition2 [branch2]
]

Having transformations 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.

So, hmm.

...Or Are Void Variables The Problem?

There could be some kind of rule, that variables are not allowed to hold voids...and if you assign void to a variable it becomes null.

 >> void? x: if false [<a>]
 == ~true~  ; isotope

 >> x
 == ~null~  ; isotope

Proposals along these lines have been entertained before. I don't think I like it, and I feel like void variables should be legal.

I Think I'm Committed To Untaken Branches As VOID...

But as with most things here, I think the fun is letting people play language designer and making their own choices.

If pure VOID means no branch taken, you can pipe the constructs:

case: chain [:case, :void-to-null]

And that's it. You could choose to do this for CASE and SWITCH so they'd need a MAYBE to vanish quietly, and leave IF alone.

But it's important to look at all the angles...!

2 Likes