Should Failed IF Be NULL (and Break My Heart 💔)?

This idea has been near and dear to my heart: that without any additional work, failed IF returns something that will vaporize in COMPOSE and UNSPACED.

But over time the costs have been adding up. Are these costs too great? Let's look.

It makes IF different from all the other branching constructs

e.g. CASE and SWITCH return NULL when they don't take a branch. IF being different is a design/cognitive load, some might argue.

What if you actually want an IF that returns NULL?

For instance, setting refinements in a frame:

>> bunch-of-dups: 'yes

>> f: make frame! append/

>> f.series: [a b c]
>> f.value: 'd
>> f.dup: if yes? bunch-of-dups [5]

>> eval f
== [a b c d d d d d]

It's not legal to make f.dup a void, so in this situation we want if null [5] to be null.

There are workarounds of course:

f.dup: either bunch-of-dups [5] [null]

f.dup: all [bunch-of-dups, 5]

Historical Redbol of course biased this the other way, which is why when you want a COMPOSE slot to go away that's when you have to use either condition [...stuff...] []

Branches Weirder Than They Need To Be, Maybe?

THEN and ELSE MUST be weirder than (deferred) infix IF and IF NOT... because their treatment of "heavy nulls" (e.g. antiform blocks with quasi-nulls in them) differs.

 >> if (pack [null]) [print "This does not run"]

 >> (pack [null]) then [print "This runs"]
 This runs

This is a weird little design edge of necessity, that branching constructs turn "plain null" into "boxed null" in order to signify that a branch ran. We do not want branches that incidentally evaluate to null to lead to else branches running:

>> if 1 = 1 [print "BRANCH", x: null] else [print "ELSE"]
BRANCH
ELSE  ; !!! Noooo !!!

But THEN and ELSE are reacting to either unboxed void or unboxed null. Given that NULL is now the only falsey state, might it be better if unboxed null was the only state they reacted to?

There Could Be Shorthands...

OPTIONAL has a shorthand as the ? operator, and this would fit IF into the same strategy as other constructs:

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

>> unspaced ["a" ? if null ["b"] "c"]
== "ac"

Or maybe the voiding IF has a special name. Like IFF for "IF-and-only-IF", meaning you don't intend to use it with THEN or ELSE, so it might return VOID which they (conceivably?) error on if they're testing for "falsey" ness?

>> compose [<a> (iff null [<b>]) <c>]
== [<a> <c>]

>> unspaced ["a" iff null ["b"] "c"]
== "ac"

Possibly Creative Concepts From if (if ...) [...] etc.

Turning IF back into something that returns a falsey null permits things like if (if ...) [...] and other creative concepts, and it's not like there aren't uses in aggregate expressions for an IF that gives falsey results instead of opt out on not branching.

Since both are useful, being able to give an inch here and split it out into two different constructs might be the good answer...making IF like every other conditional construct that returns null on branch not taken?

Something Big (May) Affect This Question

The stable antiform state currently known as "~void~" is losing the name "VOID" to the unstable antiform state that has been called "NIHIL" (of late).

Should VOID Assigns Mean "Fully Remove A Key"?

It's not the first time empty antiform block will have been called "void". :roll_eyes: But it's the first time where I've really seen the choice with true clarity.

So, what are the implications of this?

>> if 1 > 0 [<hmmm>]
== ~[]~  ; anti

There's one bad impliciation... and it's pretty bad indeed, I'm afraid. If ELSE is to work in this case, we have to make it so that empty packs themselves get packed:

if 1 < 0 [pack []] else [print "Don't want this to print..."]

The branch can't return an empty antiform pack if that triggers else. It has to return a pack inside a pack, e.g. unstable antiform ensconced in an unstable antiform. :frowning:

>> if 1 < 0 [pack ~[]~]
== ~[~[]~]~  ; anti

Such things are legal to construct (they have to be). But I'm wary of them decaying automatically. And having them arising in such simple circumstances is a deal breaker:

>> if 1 < 0 [if 1 > 0 [<ack>]]
** Error: Cannot automatically decay ~[~[]~]~  ; antiform

The alternative would be that we get voids as a THEN-trigger (so only null doesn't trigger THEN, and triggers ELSE) or that they are errors. I don't think they should be errors, because I believe loops that don't run their body ever should return void... and I want you to be able to write:

while [...] [
   ...
] else [
   ; whatever you want done if there's a BREAK
]

In the past the handling for a loop that never ran its body was that it would run the ELSE. But I feel now that shouldn't be the case--ELSE should exclusively mean it encountered a break.

So It's Settled, My Heart Has To Be Broken :broken_heart:

  • Unstable antiform VOID triggers THEN, and not ELSE... like all other PACK!.. emptiness makes no difference.

  • IF returns NULL, like every other branching construct.

What Makes Voids If Not IF?

While I proposed IFF I now realize that is a really bad idea. It's way too easy to just see an IFF and go tack on an ELSE clause without thinking "hey, I can't use ELSE with that".

So it would have to be a completely different word. I like WHEN

>> compose [a (when 1 > 0 ['b]) c]
== [a c]

A design idea I had here was that this would be distinct from ? IF, because if you put the OPT on the outside of the construct, then you turn any null into a void... even if it originates from the branch.

>> ? if 1 > 0 [<foo>]
== ~[]~  ; anti

>> ? if 1 < 0 [null]
== ~[]~  ; anti

But I thought WHEN would be used when you expect the branch to produce a value... and catch cases when it didn't, by letting it produce NULL. It wouldn't have to box the null... which means you could use WHEN with ELSE, but it would only run the else clause if the branch was taken and produced null...

Is there another word that would convey this property? :frowning:

>> xxx 1 > 0 [print "ran branch null] else [print "ran else"]

>> xxx 1 > 0 [print "ran branch null] then [print "ran then"]
ran then

>> xxx 1 < 0 [print "ran branch" okay] else [print "ran else"]
ran branch

>> xxx 1 < 0 [print "ran branch" null] else [print "ran else"]
ran branch
ran else

That's a pretty weird set of properties to exploit... weird enough that it would be nice if you could actively prevent using it with THEN or ELSE. But there's only one way to do that, which is to return antiform comma (a "ghost") and act like a comment. This would work in the specific case of COMPOSE if it was the only thing in the slot, but not if there were any code--even a PRINT statement--before it.

I guess the word WHEN doesn't go with THEN or ELSE verbally (when ... then) or (when ... else) so maybe that's enough of a clue you shouldn't do it unless you are a weirdo and think you know what you're doing. :zany_face:

Two months downstream of changing IF to return NULL, I'm now bringing the bootstrap code up to date.

It has to run in a world where a non-branch-taking IF does not vaporize in expressions, because it no longer returns VOID. So the IFs have to be OPT'ed or ?'ed or use WHEN instead.

It's a lot fewer changes than I thought, and usually you can write the code other ways to avoid it. But still, the changes definitely make me sad. I don't like the IF vs. WHEN distinction, and it wouldn't exist in a perfect world.

So I want to look at this again and ask if this is truly the last word--if there is no other option.

It seems now that non-meta assignments of void, e.g. f.dup: void is going to be the way to make the field unset. That would mean it would do whatever an unspecified refinement would do, e.g. not be taken.

And actually...I'm somewhat torn on whether NULL is what is used in frame building to opt out of refinements (or an <opt> parameter) or not. This kind of hinges on whether when the frame is built, if the <opt> does the transformation from void to null during fulfillment, or during type checking. If you literally have to assign parameters to void, then to truly opt-them out (or specialize them out) you would have to use f.^dup: void ... which seems kind of crazy, but, you're asking to drop a refinement off the interface...which you didn't have to do--you could have left it alone and it just wouldn't have been supplied.

Anyway, this is no longer as clear-cut...and in fact, VOID may be necessary.

This is kind of the big deal.

:thinking:

I've been saying you can't unpack unstable antiforms automatically. But... how bad is this? What do you expect if you say:

x: if condition1 [if condition2 [...]] else [1020]

In the case that condition1 is true and condition2 is false, what did you want X to be? Is the error really that bad?

Not being able to unpack a pack doesn't bother the evaluator if you let the result drop on the floor, just if you try to use it.

But if we made branches pack up voids that refused to decay without explicit unpacking, that rules out things like:

 append [a b c] case [
     condition1 [void]  ; note: same as `condition1 []`
     condition2 [1020]
 ]

When VOID works here, you get the nice property that it will still error by default if none of the branches are taken (due to CASE returning NULL on no branch). And then the VOID can opt out.

If that packed the branch so it wouldn't decay, you could still write something like:

 append [a b c] opt case [
     condition1 [null]
     condition2 [1020]
 ]

That would box the null, but the OPT would also turn a null from no cases into a void, so you'd not get the natural protection from no cases matching.

Looks like Broken Heart May Has To Stay Broken :heart_exclamation:

This comes down to a contention between "IF being able to return void for no branch" vs. "branches being able to return void".

For THEN and ELSE to react to VOID without reacting to a branch that produces void, you wind up boxing an unstable pack in an unstable pack. Making such an antiform on a casual basis is just not something viable to do.

So as much as I may not like the IF vs. WHEN distinction, it's just one of those things. If you don't like it, there's opt if and ? if, though they're not precise synonyms for WHEN, since they return void should the IF run a branch and evaluate the branch to null.

And you've still got the historical Redbol-style answer, of:

either condition [...] []

It's a pain point, but it just looks like the pain of making ELSE reactive to VOID when it's an unstable antiform creates much more pain. Being able to say ELSE just reacts to "light" NULL--the only falsey value--solidifies the system in other ways.