The Siren Song of the Arity-1 IF


#1

We need a name for “arity-1 IF”. This is the thing that turns falsey things into NULL, but passes through truthy things.

It’s really, really tempting to call it… IF :thinking:

>> if 1 = 2
;-- null

>> if 1 = 1
== #[true]

>> if <whatever>
== <whatever>

You’d have to couple it with something else like a THEN to use it in the traditional way. That introduces some annoyance, like where would you put the THEN?

if x = 10 then [
    ...
] else [
   ...
]

if x = 10
then [...]
else [...]

So it’s a terrible idea…or is there another angle here?

Well, let’s back up a second. I mentioned we’re going to need this arity-1 construct one way or another… but I didn’t say there wasn’t still going to be an arity-2 form of IF.

Imagine we renamed today’s “arity-2 IF” to “WHEN”. So these would be equivalent:

if x = 2 then [print "x is two"]
when x = 2 [print "x is two"]

Putting those two side by side, if x = 2 [print "x is two"] starts to feel incomplete, and WHEN sounds like a merging (if you say “ifthen” real fast it sounds like “when”).

Something here feels like it couldn’t be the other way around. When you think of a small atomic box that just does one thing… giving the super short word that duty has a fair amount of draw.

IF/THEN/ELSE seem like they go together

If your language has an IF, and a THEN, and an ELSE… I think the natural assumption would be that you would put the THEN after the condition of the IF.

But that’s not today’s IF. You can use THEN with an IF should you want to…but you’ll be adding more code after a branch that already ran:

if 1 = 1 [
    ... code runs ...
] then [
   ... more code to run since last branch ran...
]

So you’d use THEN with a CASE or SWITCH, but not today’s IF…but that feels weird.

Arity-1 IF would correct this schematic. And wouldn’t it be easy to frame it as “WHEN is like an IF with the THEN built-in”?

PARSE’s IF is arity-1

PARSE’s IF just takes a condition. It confused me, and has been a source of confusion for others.

Previously I’d suggested making PARSE’s IF consistent as arity-2. But the consistency could go the other way as well, if both were arity-1.

It deepens the illusion

A big part of my affinity for the Rebol design space is how garden-variety the language can often look on the surface, when it’s working in a much stranger way…with that strange mechanic affording a lot of degrees of freedom in compositionality.

Having IF be a “vaporizer of falsey things” is a good trick and takes this composability further.

>> if 1 = 2 else [print "what's not to love?"]
what's not to love?

The possibilities are getting kind of mind-boggling. Earlier in Ren-C there was a version of IF called IF? which would either run the branch or not, but return the truth or falsehood of the condition–not what the branch evaluated to. With arity-1 IF you can pass through the condition’s truthy or falseyness by using ALSO:

>> if <truthy> also [print "Running" <discarded>]
Running
== <truthy>

>> if 1 > 2 also [print "Running" <discarded>]
;-- null

ALSO is like a THEN that throws away its result, passing through the condition. So there you have it, an IF construct that evaluates to its condition (if truthy) or null. Wild, huh?

So like I say, I don’t know what to call this, if it’s not IF

Maybe everyone would ignore it and use WHEN? I don’t know. I imagine I’d probably use a mixture.

But the case looks solid to me. This feels like what IF needs to be.


#2

This would be a big change, but looking back through random snippets of old code then it it looks like it would work well for me.
An example of where it improves clarity is when using if … all e.g.

if all [
    test1
    test2
    test3
] then [
  do-something
]

The then really improves the readability here.


#3

As it has turned out, you don’t even need the IF there, since ALL returns NULL on failure:

all [
    test1
    test2
    test3
] then [
    do-something
]

But that furthers the point that the menagerie of constructs means there’s less pressure on IF to be all that brief. It has been a historical source of complaint that if people are writing piles of IF statements then they are probably “not getting it”.


#4

There’s a mechanical problem with this proposal, regarding interaction with comparison operators.

When you write:

if x = 1 then [print "x is one"]

An arity-1 IF can consider itself to be fulfilled at the moment of if x. And since = has been changed to be “left enfix normal” it will gladly force completion of the left. You get:

((if x) = 1) then [print "x is one"]

That’s bad. You don’t want the IF running before the = is finished.

There is a mechanism for disallowing operations on the right to force completion, which is used by <-. But that won’t bail us out here, because that would break THEN, and you’d get:

if (x = 1 then [print "x is one"])

So this is tricky to fix.

Could =, <, >, etc. go back to being tight left enfix?

If we went back to historical Rebol’s “tight” behavior with =, then it would be greedy…and wouldn’t allow its left hand side to complete. You’d get the expected “precedence”

(if (x = 1)) then [print "x is one"]

But…I’m actually attached to comparison operators being left normal. :-/ It really improves the readability of a lot of things. If I’m forced to pick between that readability and any of the purported advantages of arity-1 IF, off the top of my head I’m pretty sure left normal comparison wins pretty easily.

Some might remember that when the comparison operators were changed to complete their left hand sides, there was a casualty for another arity-1 operation… NOT. It seemed that it was getting its “precedence raised”, because not x = y was suddenly acting as (not x) = y. That was tolerable… because it was consistent with other languages, e.g. C’s !x == y is (!x) == y and isn’t !(x == y)`.

Yet there’s no language I can think of where IF is higher precedence than a test for equality. :-/ Also, if we were willing to sacrifice the new-and-more-pleasant comparison operators that complete their left, there’d still be things like OR, and AND. For these to serve as the logic-driven complements to ELSE and THEN, they have to be left enfix normal. See the adjustment that had to be made to SPLIT to see a nifty use of AND completing its left.

What if IF’s condition was in a BLOCK! / GROUP!?

It seems you’d have to require it, because it would be easy to forget.

This wouldn’t mean WHEN would have to, nor would CASE statements start needing them. Just IF. You’ve already got the THEN, so, what’s two more characters? :stuck_out_tongue:

if (x = 10) then [...]
if [x = 10] then [...]
when x = 10 [...] ;-- wouldn't need it

Possibly not as crazy as it sounds. Note that PARSE’s IF requires a GROUP!.

Also, what we learned from AND and OR was that hard-quoting a conditional and then letting it be either a GROUP! or a BLOCK! offers a degree of freedom. But I’m not sure what degree of freedom might be interesting for an IF.

What if IF had lower precedence than = but higher than THEN?

I think this undermines what the language attempts to do on the whole.

Rebol past and present has no “precedence” in the conventional sense (of an operator precedence table). As I said above while advocating for arity-1 IF, the point is to get a system that gives a lot of the look and feel of what you’d think was governed by complex rules–but to not actually have those rules powering it.

But in terms of what “precedence” there is, arity influences it. Being arity-2 lowers IF’s trigger point as far as operators in the condition slot are concerned: it knows its not finished gathering its arguments while a condition is in effect.

In the pro-and-con list of competing interests, this may be the talking point for explaining why IF must be arity-2. So I guess we’ll leave it at that for now… but it’s good to have it drawn up in case some other realization comes along. When design points reshuffle, we can look at the fallout and see whether some desirable thing from the past becomes suddenly possible–and this may be something to come back to if the enfix mechanics get some clever twist in the future.