Adding Sugar for Preconditions and Guards

Over the years there have been some requests to shorten something like this:

guard let foo = foo else {
    return
}

to something like this:

guard let foo

While I think that syntax is probably a little too sugary, lately I’ve been writing a lot of code that makes preconditions explicit:

guard let foo = foo else { preconditionFailure() }

In general I’m a fan of using precondition() and assert() a lot more than fatalError(). It would be excellent if this were valid:

precondition let foo = foo
// do something with foo

Under the hood, this would do the same as the guard statement, but by using precondition I think it avoids overloading the guard statement. We could also make it look like this:

precondition(let foo = foo)

as long as foo continues to be unwrapped in the scope following that statement.

Using -Ounchecked could crash in this case with an unexpected nil, but I think that would be expected. What do y’all think?

This is precondition(foo != nil) and then using foo! in the scope following that statement, is it not?

1 Like

Yeah, pretty much. I’d rather not have the ! if I can avoid it.

But this is exactly what the ! is for--why avoid it?

It's extraneous noise if the compiler has already proven that it will never be nil.

4 Likes

A good argument in favor of flow-sensitive type narrowing in the general case.

However, in this case specifically, if precondition(foo != nil); let foo = foo! is too noisy, then a straight-up let foo = foo! suffices, and if the latter is too terse, then the former provides the desired noise, no? (Not the mention the full guard let foo = foo else { preconditionFailure() } form.)

What I'm trying to say is that the language already has a spectrum of "noisiness" for this. That some of these make (correct) use of the ! operator isn't a problem that requires fixing.

3 Likes

No, it is not. foo can be non-nil on the first read, and nil on the second, causing an unexpected nil while unwrapping crash.

if let and guard let both reads a single time and binds the result to a new constant.

6 Likes

That would require some deliberate work to make possible: foo cannot be nil and then not nil from one line to the next unless it's been captured and shared across threads. Since Optional is a value type, that's provably not the case unless you've just done it to yourself within the local scope. Am I wrong?


In the simplest case, you can see that the compiler generates the same code:

func f(_ x: Int?) -> Int {
    guard let x = x else { preconditionFailure() }
    return x
}

func g(_ x: Int?) -> Int {
    precondition(x != nil)
    return x!
}
output.f(Swift.Int?) -> Swift.Int:
        test    sil, 1
        jne     .LBB1_2
        mov     rax, rdi
        ret
.LBB1_2:
        push    rbp
        mov     rbp, rsp
        ud2

output.g(Swift.Int?) -> Swift.Int:
        test    sil, 1
        jne     .LBB2_1
        mov     rax, rdi
        ret
.LBB2_1:
        push    rbp
        mov     rbp, rsp
        ud2
1 Like

foo may be self.foo which may have been mutated. Or it could be a computed property.

3 Likes

That's a good point. I glossed over the scenario where foo, orthogonal to this topic here about optional unwrapping, is a mutable or computed property such that you'd want to access its current value once and bind the result. It is indeed always good advice to be mindful of that.

When self is shared across threads, then indeed self.foo could mutate from one line to the next; if self.foo is a computed property that's not pure, then each access could give a different result.

I took it as by construction that we were discussing the scenario where we had a unique reference to a value foo, as is the case when it's passed in as an argument. It is important for users to know that foo does not change from nil to non-nil from one line to the next.

But yes, you are right that where actually binding a value up front is important, then it is important actually to bind it up front. In that case guard let cannot be replaced by two accesses. In that case, what's discussed here is then equivalent to let foo = self.foo; precondition(foo != nil) and then using foo! in the scope following that statement.

4 Likes

I'm still a fan of guard let foo and if let foo.

It is much more in line with Swift's tendency to drop boilerplate and template if it can be inferred. It fits with us writing let x = "String" instead of let x: String = "String" and leaving out the return for a single expression function, method, or computed property.

Is it very different from the following in principle? -

let foo = foo!

Since there's no custom assert message in your proposal I suppose it's equivalent.

5 Likes

All for improving guards/preconditions!

IMO this feels like extra syntax where something like this would be much nicer:
precondition(let foo = foo, "Must have foo")

Although I'm not sure how this would work, as it'd be a much more general approach, and how it'd work in conjunction with (for example) @autoclosure.

To maybe rephrase a bit: why add more "magic words" to the Swift syntax, instead of a mechanism for functions to provide such behaviour. Because if you're adding it for guard & precondition, maybe also add it for assert? Or other such functions I'm unaware of?

2 Likes

I know it's not the answer you're looking for, but just about all of my swift code doesn't have unnecessary empty unwraps. Usually it's a precondition resulting in a non-void return value. When reviewing code, seeing a line like that is code smell for me. If the function takes an optional and returns early without doing anything, then maybe it shouldn't take an optional value in the first place. I've battled with this a few times before, but if it really can't be avoided I always suggest to use that empty line before return to print a log message.

If anything the block in guard could adopt SE-0255: Implicit Returns from Single-Expression Functions, and then the syntax would be:

guard let foo = foo else {}

Which is almost exactly as long as:

precondition let foo = foo

While also letting git diffs look pretty when someone eventually does add in that print statement.

1 Like

This thread is giving me SE-0217 vibes. I feel that syntax could be quite fitting for this proposal (considering the existence of ?? as a "soft-fallback"):

let foo = foo !! preconditionFailure("Foo must not be nil")

This also allows flexibility for other cases such as:

let foo = foo !! fatalError("Foo must not be nil")

And in general, any function yielding -> Never.

I am aware of the decision to reject the original proposal 3 years ago, however most of what was said to be the reasons for it back then has fallen short by now and we're still dealing with this same issue in Swift nowadays, so I feel a review of this situation is due.

1 Like