Should THEN not "voidify" its branch (meaningful THEN-chaining?)


#1

With the following code and today's THEN, if condition is true, then all the branches will be assured of running:

if 1 < 2 [
    print "this runs"
    null  ; even if the branch evaluates to null
] then [
    print "this also runs"
    null  ; even if there's a null here, too (!)
] then [
    print "this runs too"
]

The only way the THEN branches wouldn't run would be if the previous result were null. But an IF whose condition is true will never return null--if the branch produces NULL, it will be turned into a VOID!. Since THEN follows the same policy, you get into a chain of always-running.

So even though it's THEN's result being propagated on to the next THEN to see, it's "telegraphing" the success or failure of the condition by ensuring that result is not null. This has permitted the following pattern:

 case [
     thing1 [...]
     thing2 [...]
 ] then [
     ...
 ] else [
     ...
 ]

What happens is that whether the CASE succeeded or not becomes the driver of the ELSE's decision, not the result of evaluating the THEN branch. This is not very useful for IF, because most of the time you could have added code to the success case by putting it in the branch. But for something like CASE, you're adding code that runs when any of its branches succeed--and that is interesting, because there's no single place inside the case you could do that.

For the sake of sanity, IF must voidify

You might wonder why voidify at all. Can't the people who write branches be savvy enough to make sure their branch doesn't return NULL, and just live with the consequences of the ELSE running in the odd case that it does? (Maybe they even wanted that...the ability to have a null signal to the ELSE come from either the condition failing, or a decision in the branch?)

But it's not an odd case. Having a branch evaluate to NULL is common, since conditional expressions generate them:

if condition1 [
    if condition2 [...]
] else [
   ...
]

Without voidification, you're hitching that outer ELSE to the result of the inner IF. When condition1 or condition2 are false, that ELSE runs.

This strikes me as counterintuitive. I wouldn't want to have to remember to write:

if condition1 [
    if condition2 [...]
    void  ; this would be easy to forget!
] else [
   ...
]

But is THEN a different beast?

If you look at a chain like if condition [...] then [...] then [...], might you argue that there is something fundamentally different about expectations of the second THEN than the first? It's certainly the kind of thing that would make you pause when you first saw it, to ask "what does that mean?"

(FWIW, the Promise chains in JavaScript are designed to work by taking each .then() and feeding it from the previous .then()...they don't all run just because the first one ran.)

The first is taking a branch off of an IF, so it can think of itself as an "IF-THEN". But might the second see itself as more of a "THEN-THEN?", where it's reacting to the THEN's branch evaluation, as opposed to the condition that triggered it?

This changes the dynamic for users of the if condition [...] then [...] else [...] pattern mentioned in the beginning of this post. The else is becoming responsive to the then's branch, it's a THEN-ELSE not a condition-else.

There's also, uh, ALSO

ALSO exists as a way to not affect the result:

case [
   1 > 2 [<branch-one>]
   1 < 2 [<branch-two>]
] also [
   print "I ran!"
   <also-branch>
]

That will print "I ran!" and evaluate to <branch-two>. Unlike THEN, it discards its own result.

So people who wanted the "telegraph the prior's success" behavior have a much more direct way of doing it. Use also [...] then [...].

(There's no parallel for a "telegraphing ELSE", because the only thing it would telegraph would be the null that triggered it. If you want a telegraphing ELSE, you can say else [... null], which seems better for this esoteric-seeming need than coming up with a keyword for it. I doubt anyone will have a great suggestion for a name for this, because any name seems less clear than just saying that.)

Evidence seems to point in favor of changing this

I think people have a very strict model of how if condition [...] else [...] should work. The ELSE is reacting to the condition, not to the what the branch produced in particular. Voidifcation gives us the best of both worlds...cued by a branch result that is usually passed as-is...but tweaked in the rare case that it needs to be in order to signal a distinct state from null.

But then [...] then [...] doesn't come with these expectations. When you look for the clause driving it, then it seems reasonable to look one unit to the left...and be sensitive to the branch's result. If the branch ended in a conditional, and that conditional failed, it seems plausible to imagine the conditional inside of the branch having an effect on the outside.

The argument for then [...] else [...] acting as it has comes somewhat from the idea that "Other languages that have both THEN and ELSE would pair them in such a way that they both reach back to react to a root condition". But Rebol doesn't need a THEN for its IF--you're already dealing with something different. So just as you have to learn it's not if condition then [code], you have to learn that then [...] else [...] is the ELSE reacting to the then's outcome.

Really this is just bringing THEN more in sync with ELSE, which already does not voidify.

The mitigation is not that difficult

If you're want to write then [...] else [...] and have it act as before, just make sure the THEN branch doesn't itself return NULL. It will evaluate to null if what was triggering it was null, so the only case you have to worry about is a branch evaluating to null.

So you'd be fine with something like this:

   ...
] then [
   reduce [...]
] else [
   reduce [...]
]

Blocks aren't null. If you're passing back a result, most of the time the results aren't null...and if you're not passing back a result, then you can use also:

   ...
] also [
    ...
    if condition [do side-effect]
] else [
    ...
]

Like I say, this seems pretty learnable. Just means an ELSE following a THEN may not have the relationship one might stereotypically expect. But it seems well grounded in rationale, and it's a drop in the bucket of "other language unlearning" that is required (!)


#2

Well, here's one (terrible?) name suggestion... ELSO. "It's like the ELSE form of ALSO." :slight_smile:

if 1 > 2 [
    print "Won't print or run"
] elso [
    print "Will run"
    <elso-result>
] then [
    <then-result>
]

That would give you null, because even though the ELSO triggered on a null and its branch evaluated to a TAG!, it threw away the tag to telegraph the null...which THEN sees instead of <elso-result>.

I suppose the project to come up with a better name for this can use the codename "ELSO", so we at least know what we're talking about: an ELSE (e.g. null-triggered construct) that discards its result and always returns null.