3

// Doing let first followed by a bool check in guard statement results in compilation error

   self.action = {  [weak self] in
      guard let `self` = self, data.isJSON() else { return }

// Doing bool check first and then let works

  self.action = {  [weak self] in
     guard data.isJSON(), let `self` = self else { return }

The two statements above seem equivalent to me. Why will it not work in the first case?

Sulthan
  • 128,090
  • 22
  • 218
  • 270
Boon
  • 40,656
  • 60
  • 209
  • 315

1 Answers1

4

Re-constructing your question into a minimal, complete and verifiable example

First of all note that it would be preferable if your question contained a minimal, complete and verifiable example (mvce), which, in its current form, it does not:

  • the closure list is irrelevant and only confusing here
  • the unknown self (... no context) is, likewise, only confusing and of no relevance to this question

Instead, a mvce of your question could've bee constructed along the lines of:

func foo(bar: Int?) {
    // 1. why does this cause a compile time error?
    guard let baz = bar, true else { return }

    // 2. whereas this does not?
    guard true, let bax = bar else { return }
}

The answer below will discuss this mvce, rather than the obscured example in the original question. Finally note also that the guard/guard let statement is not entirely relevant (unique) w.r.t. the core of the question, as we see the same behaviour for if/if let statements. The answer below will use guard statements.


Now, can we redeem the compile time error in 1. above? And, are these two statements really equivalent?

The compile time error for the 1st guard statement in the function foo(...) above is quite telling

Boolean condition requires where to separate it from variable binding.

Fix it: replace , with where

This is also stated in the Language Guide - The Basics - Optional Binding

You can include multiple optional bindings in a single if statement and use a where clause to check for a Boolean condition. If any of the values in the optional bindings are nil or the where clause evaluates to false, the whole optional binding is considered unsuccessful.

Hence, if we want to use a condition-clause after optional binding in an guard or if statement, you need to use a where clause to separate the condition-clause from the preceding optional binding.

func foo(bar: Int?) {
    // 1. ok, compiles
    guard let baz = bar where true else { return }

    /* 2. or, include a conditional-clause prior to the
          optional binding, but is this really equivalent..? */
    guard true, let bax = bar else { return }
}

These two are, however, not really equivalent;

  • statement 2. above allows us to short-circuit the guard statement in case the initial conditional-clause turns out to be false (then not proceeding to the optional binding, but rather directly into the else block of the guard statement)
  • whereas 1. above allows the reverse: check the conditional-clause only if the optional binding succeeds.

There are naturally cases in which we would prefer one over the other, e.g. if the conditional-clause contains some really heavy calculations, we might not want to execute these unless we're certain that the optional binding succeeds

let heavyStuff: () -> Bool = { print("foo"); /* ... */ return true }

func foo(bar: Int?) {
    /* 1. call the heavyStuff boolean construct only if
          the optional binding succeeds */
    guard let baz = bar where heavyStuff() else { return }

    /* 2. possibly unnesessarily perform heavy boolean
          stuff prior to failing the optional binding */
    guard heavyStuff(), let bax = bar else { return }
}

A less contrived example is if we want to use a successfully binded variable in (here: as an argument) in the conditional-clause that follows

let integerStuff: (Int) -> Bool = { _ in /* ... */ return true }

func foo(bar: Int?) {
    /* 1. call the integerStuff boolean construct only if
          the optional binding succeeds, using the binded
          immutable as closure argument */
    guard let baz = bar where integerStuff(baz) else { return }

    /* 2. ... not really any good alternatives for such 
          flow if using this alternative */
    guard integerStuff(baz ?? 0), let bax = bar else { return }
}

Finally note that, technically, if you really want to separate an initial optional binding with a following conditional-clause, you could use dummy case let (non-optional always-succeeding variable binding/assignment) statement coupled with a where keyword for your conditional-clause

let checkThis: () -> Bool = { /* ... */ return true }

func foo(bar: Int?) {
    // ...
    guard let baz = bar, case let _ = () where checkThis() else { return }
}

This is, however, just to show this technicality; in practice, just use a where clause.

Community
  • 1
  • 1
dfrib
  • 70,367
  • 12
  • 127
  • 192