R3-Alpha had an idea--carried forward by Red--of an arity-1 IF combinator.
red>> num: 1020
red>> parse [a a a] [if (even? num) some 'a]
== true
red>> parse [a a a] [if (odd? num) some 'a]
== false
As you see, if the expression you give it turns out to be "falsey" then it doesn't continue matching. It skips to the next alternate--if there is one.
red>> parse [a a a] [if (odd? num) some 'b | some 'a]
== true
But I always thought the arity-1 IF was a pretty alien thing that would confuse people. You might think there's a branch, but there's no "branch"... just continuing along with the variadic list of everything that follows until the next | or end of BLOCK!.
I also wondered "where does it end?" With an IF combinator, why not a CASE combinator, or SWITCH combinator?
So when I came up with GET-GROUP! doing arbitrary substitutions of the rule it evaluates to, I thought "hey, that's a lot more general!" We could just say that ~true~ and ~void~ antiforms would continue the parse, ~false~ would stop it, and ~null~ antiforms would trigger an error in case you didn't mean to do that.
What That :(GET-GROUP!)
Concept Looked Like
(Note that if condition '[...] is equivalent to if condition [[...]]. This is called "soft-quoted branching")
>> num: 1020, rule: null
; generated [some 'b] rule is treated as if it had been written there
>> parse [a a a b b b] [some 'a :(if even? num '[some 'b])]
== b
; generated ~void~ from non-taken IF gets ignored, and it kept parsing
>> parse [a a a b b b] [some 'a :(if odd? num '[some 'c]) some 'b]
== b
; generated ~true~ signal continues parse, just as ~void~ did
>> parse [a a a b b b] [some 'a :(even? num) some 'b]
== b
; generated ~false~ skips to next alternate (isn't one, so parse fails)
>> parse [a a a b b b] [some 'a :(odd? num) some 'b]
** Error: PARSE BLOCK! combinator did not match input
; treat ~null~ conservatively, use :(maybe rule) for ~void~ to keep going
>> parse [a a a b b b] [some 'a :(rule) some 'b]
** Error: ~null~ antiform generated by GET-GROUP! in PARSE
Flexible Logic Kills [~true~ ~false~]
... Breaks That Idea
In the flexible logic model, [TRUE FALSE ON OFF YES NO]
are WORD!s, and hence indiscriminately trigger taking the branch in something like an IF when used directly. The ~null~ antiform is the "branch inhibitor", and it's what conditional expressions return when they don't match the condition.
>> 10 > 20
== ~null~ ; anti
I don't think it's a good idea to make substitions via GET-GROUP! (or whatever comes to replace it) silently continue on NULL. If you forgot to set a variable that was supposed to hold something (as in rule above), that should give you an error. But I don't think you should have to write :(maybe even? num)
So Having A Conditional Logic Combinator Makes Sense
I just think that IF is a rather lousy name for it.
So I'll suggest WHEN.
>> parse [a a a b b b] [some 'a, when (even? num), some 'b]
== b
It would be against the premise of flexible logic to have WHEN be biased and assume things like TRUE, YES, or NO should mean it continues or not. I like the idea that you could hold a completely arbitrary word in a variable and say when (word)
, that means "continue matching when word is set to a non-null value".
Hence you'd have to say when (true? flag)
or when (off? toggle)
etc. I'm not merely comfortable with this... I am gung-ho about it!
(Of course people can make their own combinators and build in biases of their choosing, the core just doesn't pick sides.)
BYPASS Can Be A Synonym For [when (null)]
I didn't like using FAIL for saying when to stop a rule chain and go to the next alternate, because that is used for causing "abrupt failures" in the system.
So I'd been using quasiform ~false~
the state in source (and the antiform if in a variable).
>> parse [a a a b b b] [some 'a, :(if even? num [false]), some 'b]
** Error: PARSE BLOCK! combinator did not match input
>> parse [a a a b b b] [some 'a, ~false~, some 'b]
** Error: PARSE BLOCK! combinator did not match input
But that isn't the model anymore. There is no ~false~ or ~true~ antiform. And honestly it wasn't that literate anyway. when (...) makes it clearer when you're using a variable. And the quasiform just looks confusing.
Searching for a good word that doesn't run into something serving other purposes (e.g. BREAK), I asked Claude.ai for suggestions, and one of those was BYPASS.
I like it. So for example you could write:
>> parse [a a a b b b] [some 'a [:(if even? num ['bypass]) some 'c] | some 'b]]
== b
Although that particular case is clearer as [when (odd? num) ...]
, but sometimes you have to throw in a bypass rule.
(Amusingly, in Rebol2 the idiom for BYPASS was [end skip]
, which was a rule guaranteed to mismatch at any position: either you weren't at the tail and the END wouldn't match, or you were at the tail and the END would match but then you couldn't SKIP.)
Where Does It Stop?
I also wondered "where does it end?" With an IF combinator, why not a CASE combinator, or SWITCH combinator?
So I think it's good to just say WHEN.
You don't technically need WHEN if you have BYPASS to skip to next alternate, and ~void~ to keep going (or empty block, if you like... []
will keep going too).
when (cond) => :(if not cond ['bypass]) ; or :(if not cond 'bypass)
But that forces you to reverse the sense of your logic and write out something longer (and slower). I think if you've got logic that's complex like a case or switch, then writing it out as a splicing rule would have negligible benefit to try and shoehorn as a combinator.
A Potential Weak Spot In #
for Canon Branch Trigger
It's a given that the ~null~ antiform is the canon "Branch Inhibitor". It may well be the only branch inhibitor (though I'm considering ~NaN~ antiforms might also not trigger branches).
What's more up in the air is what the canon branch trigger is.
Before considering WHEN--I was looking at the impacts of using # on the GET-GROUP! substitution rules that had been in place.
Previously you could do this:
>> parse #{000000FFFFFF} [zeros: tally #{00} :(odd? zeros) some #{FF}]
== #{FF}
>> parse #{000000FFFFFF} [zeros: tally #{00} :(even? zeros) some #{FF}]
** Error: PARSE BLOCK! combinator did not match input
But this becomes fully broken with things like EVEN? and ODD? returning either ~null~
or #
.
>> parse #{000000FFFFFF} [zeros: tally #{00} :(odd? zeros) some #{FF}]
** Error: ~null~ antiform generated by GET-GROUP! in PARSE
>> parse #{000000FFFFFF} [zeros: tally #{00} :(even? zeros) some #{FF}]
** Error: PARSE BLOCK! combinator did not match input
The second case didn't match because since things like #a
are character literals, #
has been used to represent the 0 codepoint. So in BINARY! it matches that.
>> append #{DECAFBAD} #
== #{DECAFBAD00}
>> parse #{000000} [some #]
== #
(Having the # combinator synthesize # vs 0 is debatable. But it wouldn't be #{00}. We don't want matching combinators to make new series--they only return their own series argument which is already allocated.)
And if you had a BLOCK! it would match #
>> data: [a a a # b b b]
>> parse data [some 'a :(true? trailing-b) some 'b]
== b ; great! we know the block is all As and Bs! (oh, WHOOPS!)
This is part of why I was saying the canon branch trigger should be an antiform--because it gets pushed out of band for things like this.
But the inconvenient truth is that tradeoffs are inevitable. Here (and elsewhere) the problem can be addressed by not trying to mix conditional logic with substitution. Substitution needs to be either a legal array element, or a ~void~ antiform to consciously opt out. Conditional logic is now fully driven by non-nullity, meaning you need different instructions to contrast it with full-band substitution.
It still makes me a uneasy that the canon branch trigger isn't an antiform. That will inevitably cause confusion... be accepted where it shouldn't, or have unintended meanings.
>> num: 304
>> compose [flag: (odd? num)]
** Error: Cannot compose ~null~ antiform into array slot
>> compose [flag: (even? num)]
== [flag: #] ; we allowed something that is likely not what you meant
So perhaps people can be empathetic to why I thought NOTHING would be a better choice for the canon branch trigger!
But this might be an unwinnable fight, and the consequences of reusing the NOTHING antiform are greater than that of getting the occasional # substituted where it should not be...with the burden of inventing a whole new antiform not giving the payoff that putting another part in the mix needs to have.