Merits of the "unless" keyword

I'd like to reopen the idea of introducing the unless keyword. I can't find a proposal for introducing it on its own as a complementing opposite to if. I saw the decisions about using "unless" instead of "guard" and a discussion about postfix operators that mentioned unless; however, I couldn't find if we have discussed the merits of unless on its own.

Languages like Perl and Ruby use unless as the opposite of if; this improves readability in some cases.

For example, when some conditional logic is needed in the negative case.

if !items.isEmpty {
    await process(items)
}
unless items.isEmpty {
    await process(items)
}

Or even if both are handled unless, make it easier to put the more important path first, which helps readability.

if user.authenticated {
    log("user already logged in")
} else {
    await beginAuthentication()
}
unless user.authenticated {
    await beginAuthentication()
} else {
    log("user already logged in")
}

Finally, unless is different from guard because it allows the function to continue.

func processUser(user: User) async {
    unless user.authenticated {
        await beginAuthentication()
        // guard would return here
    }
    showWelcome()
}

I think unless can have a place in Swift as another option to make our code more readable.

17 Likes

I loved using unless in Perl. It made a big difference to the readability of the code.

I’d love to see it added to Swift.

4 Likes

While I'm not arguing against your points, this is not entirely true. You can just break out of a labeled do with guard.

func processUser(user: User) async {
  authenticate: do {
    guard user.authenticated else {
      await beginAuthentication()
      break authenticate
    }

    log("user already logged in")
  }

  showWelcome()
}
3 Likes

Thanks, @Quedlinbug. Maybe this is not the best example. I was trying to show that unless doesn't require an exit from scope like guard does.

It's true that the same logic can be achieved with a guard, either with a labelled do like you've shown or possibly with a defer like this;

func processUser(user: User) async {
    defer { showWelcome() }
    guard user.authenticated else {
      await beginAuthentication()
      return
    }
}

In reality, I would probably just use an if == false here so maybe using guard is a bad example.

func processUser(user: User) async {
    if user.authenticated == false {
        await beginAuthentication()
    }
    showWelcome()
}

But the point is that unless can make some functions more readable and easier to follow without needing to exit the scope.

I like the way it reads, except in unless…else cases - maybe it's because I'm not used to the unless keyword, but it seems to be non-trivial for my brain to understand the logical control flow. Merely being able to swap the if…else blocks is also not as compelling (and I say that as someone who's traditionally a stickler for putting the uncommon / error cases last, so that the happy path is most apparent).

I do often find myself wishing guard didn't require me to exit - or more specifically, that there were something like guard (not that anyone's suggested it here, but to be clear: I don't think it's a good idea to allow any variant of the existing guard keyword that allows escaping, because it would lead to mistakes - a separate keyword is important).

My only hesitation is that it does add another keyword, which - as intuitive as that keyword is (IMO) - does marginally increase Swift's complexity. Since the benefit is also relatively slight, I'm not sure where the net result falls.

Also, as an alternative: the not keyword, a la Python. Even after a decade of writing Swift I still occasionally write if not … out of habit / intuition / something, and it's never stopped being disappointing that Swift doesn't recognise this elegant syntax.

9 Likes

I like this approach better than an unless keyword. To me, negation written with a prefix ! is often too subtle when glancing at code like

if !letter.isASCII {
    // ...
}
if intersection.isEmpty {
    // ...
}
if !table.contains(.date) && !list.isEmpty {
    // ...
}

compared to the clarity of

if not letter.isASCII {
    // ...
}
if intersection.isEmpty {
    // ...
}
if not table.contains(.date) && not list.isEmpty {
    // ...
}

I have seen some code using a custom :exclamation: (the emoji) operator as an alias for ! to make the negation stand out more. The syntax coloring on this forum does something similar:

if (!isEmpty) { /* ... */ }

So, it might just be a themeing/Xcode issue. But I would welcome a not operator; not that I am expecting that to happen.

8 Likes

+1 I would really like to see this in Swift. It reads clearly to me.

A not keyword may also do the trick. I think you've highlighted the root problem here; the ! prefix is very easily overlooked. Our team prefers == false for this reason. Maybe we could remove the ! prefix altogether in the same vein as removing the C style for loops.

I personally think unless still has a small advantage once we have multiple arguments

We currently would do something like this;

if user.isLoggedin == false && hasStoredCredentional == false {
    showLoginScreen()
}

or maybe

if (user.isLoggedin && hasStoredCredentional) == false {
    showLoginScreen()
}

But with an unless it can become;

unless user.isLoggedin && hasStoredCredentional {
    showLoginScreen()
}

not is one of the commonly rejected proposals, despite its appealing to many outlook. It also would be odd to introduce just not, dismissing rest of the "family".

EDIT: yet reasons behind rejection seems to be mostly from the grammar perspective, if any – it reads much better than !

1 Like

There are some operator-like keywords like as or is, but I understand the desire/requirement to keep operators syntactically separate from other symbols.

A not keyword baked into the compiler would probably work, but introduce special cases in the grammar, make not more special than other operators, and go against the notion that the standard library of Swift is no more special than regular Swift code. I think there are enough cases already where the language can do stuff that a regular programmer cannot that the last point would not bother me.

I don’t expect not to ever land, but I think it’s addressing the issues solved by unless better than unless does.

1 Like

Quite like the idea in general. But is unless just a negation of if or does it have similar properties to !

Like would

if user.isAuthenticated && unless user.isDisabled {

work out would it be an error?

Because I think my example makes the code look more unclear, which would be the opposite of the idea.

Also does

unless user.isAuthenticated && user.isAdmin {

mean

if !user.isAuthenticated && !user.isAdmin {

or

if !user.isAuthenticated && user.isAdmin {

and to get the opposite is

unless user.isAuthenticated && !user.isAdmin {

necessary? Because that would be prone to big mistakes as every condition is now in reverse and you would now have to know the exact way unless works, whereas if is self explainatory. Translating my example to English would say

„Unless the user is authenticated and the user is not admin“

although it could mean

„Unless the user is authenticated and unless the user is not admin“

which 1) is a really confusing way of saying it and 2) would reverse the meaning of the sentence.

So to work imo, unless would need some pretty big restrictions e.g. only a single condition.

4 Likes

I think if not would have ever be introduced, than only as part of the language, not at stdlib level. It will indeed be similar to as/is in this speciality.

Agree. I am more against adding unless in general. Even though cases are familiar to everyone and it is tempting to try improving it, adding one more keyword to the family and further complicate choice of what to use is not what seems best (C++ and Ruby vibes on providing 10 different options to do the same thing).

Augustus De Morgan formalized that these are not equivalent:

I always use commas instead of the anachronistic && when possible, and think the inability to use a comma for that code is a good argument for unless, as long as these would all be equivalent:

if !(condition1 && condition2) { print("not both") }
if !condition1 || !condition2 { print("not both") }
unless: do {
  guard condition1, condition2 else {
    print("not both")
    break unless
  }
}
unless condition1, condition2 { print("not both") }
2 Likes

Tangentially, I think you're alluding to the fact that Swift doesn't [generally] allow operators to start with letters? I've often wondered what would happen if that were lifted. e.g. it'd be so nice (and so in Swift's style of type safety) to be able to write 15W * 16h and have it just work like it obviously should (because W and h could be suffix operators that are essentially aliases for Watts(_ value: Double) and Hours(_ value: Double), or similar.

So there's at least two appealing cases for allowing operators to be words (letters).

2 Likes

The

state that

Requiring the compiler to see the "operator" declaration to know how to parse a file would break the ability to be able to parse a Swift file without parsing all of its imports.

and I can see the logic behind that. An exception in form of a keyword would still work, but would go against the idea that whatever build-in operators can do is also doable by custom operators (same as with many other language features).

While that was an important idea at the start (for example, Int not being a separate, primitive thing like in many other languages), I think that principle should not hold back the language. And I don’t think it does, as exemplified by Sendable being a protocol that regular developers could not have written (like many other additions).

I think what is holding not back now is that

  • it would be strange to make such a fundamental change so late,
  • the uncomfortable status that ! then gets (is it legacy? some users will continue to use it, some not, causing unnecessary debates), and
  • specification/implementation complications.

Maybe one day SwiftUI needs something like “functions in prefix position without parentheses”, but save for that, I don’t have high hopes.

2 Likes

Personally, I'm in favor of both not and unless, but that's as a seasoned Swift developer for whom the addition of those constructs wouldn't be a cognitive burden. I'm sympathetic to the desire to have fewer keywords and sympathetic to the rationale against not.

I mean, language is free to provide any implementation that is possible only at the language level, otherwise we all could use LISP and implement whatever we want :smiley:

Introducing word-like logical operators as keywords (oh, that's sounds crazy!) is just not for Swift, being C-like language. But not only that should hold new keywords as for now. Language has got many additions and non-trivial concepts recently, that it feels wrong to load it with more new things, either this not or unless.

Couldn't unless be defined in user space? Why does it need to be a keyword?

func unless(_ x: Bool, _ f: () -> ()) {
    if (!x) {
        f ()
    }
}

unless ( false ) {
    print("do something")
}

Because control-flow statements such as return, in closures, do not return from the enclosing function:

func f1() {
  if condition {
    return // Returns from f1
  }

  // OK: not executed if condition is true.
  assert(condition == false)
}

func f2() {
  unless(condition) {
    return // Returns from the closure
  }
 
  // Always executed (and can fail)
  assert(condition)
}

Some programming languages can do that, such as Ruby. But not Swift.

3 Likes

To please both those who want a distinct keyword and those who like the generalizability of not, this is surely the moment where Swift's reserving ' can be put to its greatest use: ifn't.

By keeping it all one word, we then avoid the ambiguity about relative precedence of not when used with && and ||.

This can be generalized to guardn't, whilen't, wheren't, etc. And the pièce de résistance, the ultimate guarantee of safety (and much more concise than #if false ... #endif): don't.

17 Likes