Hedging on UNLESS for Beta/One

Not everyone has historically been a fan of the idea that UNLESS is a synonym for IF-NOT. It's the same number of characters, and it's been a source of controversy. When people go around changing if-not to unless, some people change it back because they think it made matters less clear.

I'd come up with what I thought was a more interesting usage of UNLESS, as an infix operator. It would evaluate both the left and the right. If the right evaluated to nothing (NULL) then it would use the value of the left, otherwise the value of the right would override.

>> 1 + 2 unless null
== 3

>> 1 + 2 unless <a thing>
== <a thing>

I envisioned it being a way of reordering the order you'd read code, where you could put a default first...perhaps wanting it first to emphasize the most common case:

error-id: 0 unless case [
    not block? foo [13]
    closed? connection [29]
    ...
] // don't need an ELSE here, the 0 is up-front

But using it as a defaulting mechanism has the problem that the left-hand side is not in a BLOCK!, so you are always running the default case's code. Trying to fix this by requiring the left side to be a block would look ugly, and seem a bit of a nasty "surprise":

thing: [ // looks like a block literal
   foo baz bar
   ...
   x + 100
] unless [ // oh, wait, that was *code*? :-/
   switch z [...]
]

So it seems whatever an infix UNLESS does, it probably always has to run the left-hand side. That means it's a black sheep if you try to group it in with things like ELSE and DEFAULT.

But what if it was a logical operator, more like OR/XOR?

Imagine if UNLESS was like a version of OR that favored its right hand side, instead of its left. To do this, it would stylize to always have a GROUP! on its right and disallow BLOCK!, because there's no way it can make a decision without evaluating both sides. (In that way, it would be like XOR...which can't have a BLOCK! clause on the right)

>> (print "a" <left>) or (print "b" null)
a
b
== <left>

>> (print "a" null) or (print "b" <right>)
a
b
== <right>

>> (print "a" <left>) or (print "b" <right>)
a
b
== <left>

So UNLESS would just be a tiny bit different:

 >> (print "a" <left>) unless (print "b" null)
 a
 b
 == <left> // same as OR

 >> (print "a" null) unless (print "b" <right>)
 a
 b
 == <right> // same as OR

 >> (print "a" <left>) unless (print "b" <right>)
 a
 b
 == <right> // DIFFERENT! right result wins out

Psychologically I think the "unless" suggests a test for truthiness of the right. So this is better I think...it had felt forced, to say foo bar unless if condition [blah blah].

So instead of comparing it with branching constructs like IF/ELSE (where it fell down by evaluating both sides), it's a logic operator similar to XOR and OR. And still more novel than being a synonym for IF-NOT.

Not picking a definition yet might be the best idea?

Since I just came up with this alternate idea, I can't tell you how many uses I'd find for it. I'll have to try looking around consciously looking for use cases. Maybe it doesn't come up often enough, and people will miss the old UNLESS enough to be worth bringing it back.

I'll commit the new idea, and we can try it for a while. But the work has already been done to excise UNLESS, and to safely detect old usages and give warnings. There's a native IF-NOT synonym, and it's easy enough for people who want the old definition to say unless: :if-not if they like.

So instead of feeling pressure to decide one way or another, just leaving it undefined in Beta/One and revisiting it a later time might be best.

In any case, I think I can give a definite thumbs down to the idea of putting the left hand side in a BLOCK! to make it more friendly for "defaulting". So that's out.

That looks much cleaner. I'm in that camp that unless is not a good if-not

1 Like

So far, I'm liking this new definition. It still works for the original defaulting idea, too.

You can even use it for defaulting to a logic false, since your left value overrides any falsey right hand side, such as a NULL coming from a not-taken branching construct:

 >> thing: false unless (case [1 = 2 [<nope>] 3 = 4 [<nope>]])
 == #[false]

Here's a case from some debugger code. /LIMIT is a refinement, and FRAMES is an argument to that refinement:

max-rows: 20 unless (limit and [
    if blank? frames [
        99999 // as many frames as possible
    ] else [
        if frames < 0 [
            fail ["Invalid limit of frames" frames]
        ]
        frames + 1 // add one for ellipsis
    ]
])

Cognitively it's a lot more clear to have the right hand side be an expression which is producing a truthy/falsey value.

You can contrast this with "classical Rebol style":

max-rows: any [
     all [
         limit
         either blank? frames [
             99999 ; as many frames as possible
         ][
             if frames < 0 [
                 fail ["Invalid limit of frames" frames]
             ]
             frames + 1 ; add one for ellipsis
         ]
    ]
    20
]

And you can still write it in such a way if you wanted to. But I think using UNLESS with AND organizes it in a way that makes more sense here.


In any case, with this adjustment, it's working out well enough that I think it's going to be a keeper...and the IF-NOT synonym is a waste of the word in comparison.

2 Likes