A NULL produced by a THEN is turned into its isotope...just like running any other branch. So in a chain of THENs: if one THEN runs, all subsequent THEN branches will be assured of running.
case [
1 < 2 [print "This runs", null]
...
] then [
print "Runs because it receives ~null~ isotope"
null ; gets isotopified
] then [
print "Runs because it also receives ~null~ isotope"
]
This is the final rule, and I'm going to explain why.
Non-Isotopifying THEN Was a Disaster
At one point I was using Promise chains in JavaScript, and noticed they are designed to work by taking each .then() and feeding it from the previous .then(), where any step can stop the process. In that world, they don't all run just because the first one ran.
So I thought I'd try making THEN not isotopify its branch. If it returned NULL, any subsequent THEN would not run:
; Behavior of a non-isotopifying THEN
;
>> if true [print "runs", null] then [print "runs too", null] then [print "doesn't"]
runs
runs too
It's way too easy to screw up. It bit me several times and I tried to tolerate it, but this real-world code was the last straw:
any [
not ^rules.1
find [, | ||] ^rules.1
]
then [
if not endable? bind param f [
fail "Too few parameters for combinator"
]
f.(param): null ; the THEN is thus *incidentally* evaluating to NULL
]
else [
f.(param): [# rules]: parsify state rules
]
...and that's not even hidden behind a function call--the NULL is right there. Other cases are less obvious:
then [
...
if condition [branch] ; if the condition is false, the outer ELSE won't run!
]
else [
...
]
Too often you write a branch and aren't conscious of what it produces at all. It was a terrible idea.
So THEN must isotopify NULL. How about ELSE?
Once you take away the idea of THENs being conditional on the previous THEN, it's hard to have any sensible reasoning about what the following should do:
case [
... ; (1)
] else [
... ; (2)
] then [
... ; (3)
] else [
... ; (4)
]
I think symmetry is the easiest idea to absorb... that ELSE treats its branch just like any other, and it gets isotopified. That suggests there's no way for the code in (4) above to run.
With this strategy, the only possibilities are:
-
(1) => (2) => (3)
- if the case doesn't take a branch it feeds NULL to the first ELSE, which isotopifies its result. This guarantees the THEN will run, which then isotopifies its result...never feeding pure NULL to the last ELSE. -
(1) => (3)
- if the case does take a branch it will isotopify any NULL and thus the first ELSE won't run. It has to pass through the value that it rejected, which gets to the THEN, which runs and isotopifies its result. Again never feeding pure NULL to the last ELSE.
Easy to understand. But it's pointless to put more than one ELSE in a chain.
What About a Branch Type Meaning "Suppress Isotopification"?
For a while I thought the answer might be to create a system-wide concept of requesting not to isotopify branches.
>> if true [null]
== ~null~ ; isotope
>> if true @[null]
; null
If you could do it on any branch that would cover THEN and ELSE too.
However, subverting isotopification is no longer under consideration for branching constructs. It disables building higher level conditionals out of lower level ones.
Making THEN and ELSE exceptions to the rule would wind up competing with the other interesting meanings of @[...]
branches that are chosen. But THEN and ELSE want to be compatible with all branch features!
>> if false '[we want] else '[all the features]
== [all the features]
We Shouldn't Be Mimicking JavaScript Anyway
Who wants to write a long chain of THENs when you can capture your code in a higher-level CASE-like expression, which has interesting controls for deciding how and when to run through phases?
Or build your own dialect, and inside of that dialect THEN and ELSE can behave differently. But out of the box they isotopify, and are mutually exclusive.