guarding on enum value

I am attepting to guard depending on the value of an enum...


This is a contrived example so bear with me...


public struct Invoice {
    public enum EmailStatus {
        case NeverSent
        case SentPaymentDueNotice(attempts: Int)
        case SentPaidNotice
    }

    public var name: String
    public var email: String
    public var total: Float
    public var paid: Bool
    public var emailStatus: EmailStatus
}


I would like to do something like the following...


public func email(invoice: Invoice) throws {
        guard case .SentPaidNotice != invoice.emailStatus else { // ERROR: "Variable binding in a condition requires an initializer"
            throw EmailError.AlreadySentPaidNotice
        }
   
        print("Sending invoice to\(invoice.name) <\(invoice.email)>")
    }


Unfortunately I can only seem to get this working with the compiler


public func email(invoice: Invoice) throws { 
        guard case .SentPaidNotice = invoice.emailStatus else {
            throw EmailError.AlreadySentPaidNotice
        }
    
        print("Sending invoice to \(invoice.name) <\(invoice.email)>")
    }


Which is the inverse of what I want. I can only get this behavior using if instead of guard, like...


public func email(invoice: Invoice) throws {
        if case .SentPaidNotice = invoice.emailStatus {
            throw EmailError.AlreadySentPaidNotice
        }
      
        print("Sending invoice to \(invoice.name) <\(invoice.email)>")
    }


It seems the if-case guard-case is only a binding operation, it would be nice if you could perform logical operations on it, or am I just missing something?

Accepted Reply

The way to think about this is in comparison with a switch:


switch invoice.emailStatus {

case .SendPaidNotice:

throw …


Note that the "case" is matching an expression that follows the 'switch' keyword. For "if case" or "guard case", you still need an expression, and the syntax happens to require it as an initializer:


guard/if case .SendPaidNotice = invoice.emailStatus


That's why you can't invert the test in the "guard case" statement — because you can't invert a case, period. However, as jawbroken pointed out, you don't need a case pattern at all, just a simple boolean expression. In short, all you had to do was leave out "case".


If that seems a bit confusing, I'd agree with you. The syntax of conditional patterns is hard to grasp, and I hope that a future version of Swift changes it to something a bit more predictable.

Replies

enum E { case A, B }

func guardEnum(e: E) {
  guard e != .A else { print("A"); return }
  print("B")
}

guardEnum(.B) // "B"

But if we add an associated value:


enum E { case A, B, C(something: Int) }
func guardEnum(e: E) {
    guard e != .A else { print("A"); return }
    print("B")
}


we get "error: could not find member 'A' guard e != .A else { print("A"); return }"

The way to think about this is in comparison with a switch:


switch invoice.emailStatus {

case .SendPaidNotice:

throw …


Note that the "case" is matching an expression that follows the 'switch' keyword. For "if case" or "guard case", you still need an expression, and the syntax happens to require it as an initializer:


guard/if case .SendPaidNotice = invoice.emailStatus


That's why you can't invert the test in the "guard case" statement — because you can't invert a case, period. However, as jawbroken pointed out, you don't need a case pattern at all, just a simple boolean expression. In short, all you had to do was leave out "case".


If that seems a bit confusing, I'd agree with you. The syntax of conditional patterns is hard to grasp, and I hope that a future version of Swift changes it to something a bit more predictable.

FWIW, if you change ".A" to "E.A" you get the real error, which is that there's no '!=' operator that's valid for comparing E values — which makes sense, if you think about it.

Which means that the "case"-type expression is necessary, right?

Can't you just make your enum conform to Equatable, instead? I think adding an associated value just means it's not Equatable by default.

enum E: Equatable { case A, B, C(something: Int) }

func ==(lhs: E, rhs: E) -> Bool {
  switch (lhs, rhs) {
    case     (.A,     .A):     return true
    case     (.B,     .B):     return true
    case let (.C(c1), .C(c2)): return c1 == c2
    default: return false
  }
}

func guardEnum(e: E) {
  guard e != .A else { print("A"); return }
  print("B")
}

guardEnum(.B) // "B"
  • Instead of extending Equatable, you can just have RawValue and compare it: if A.rawValue != B.rawValue { ... }

Add a Comment

Yes, you're exactly right.


The low-tech alternative seems viable too. Since there's no binding of optionals going on here, the opposite of 'guard case .A = a' can be expressed as 'if case .A = a', and there's no particular reason not to use the 'if' form — it more of a semantic matter, and what's the semantic boundary between 'if' and 'guard' anyway?

Thanks, I was hoping the new if-case would alleviate the need for implemetning Equatable on enums for situations like this.


So, do if-case and switch effectively work off of optionality?

>> So, do if-case and switch effectively work off of optionality?


I think that's a really good question, and the answer is "no". 🙂


In Swift 1:


— 'switch' worked via pattern matching, with a big selection of patterns, including boolean and non-boolean values, optionality, enum cases, types, ranges, along with value binding where it made sense to do so


— 'if' (also 'while', etc) worked via boolean values or optionality, with binding where appropriate


— 'for…in' kinda did its own thing, with binding where appropriate


In Swift 2:


— 'switch', 'if' (etc), 'guard' and 'for…in' all work with pattern matching, limited to patterns that make sense in context, along with appropriate binding. "Make sense in context" because 'switch' has multiple patterns and therefore multiple paths alternative paths of execution, but the others only have a single pattern — presumably because the syntax is complicated enough already — and so only a single alternative (apart from falling through to the next statement).


IOW, in Swift 2, basically all conditional testing has been unified into general pattern matching, and optionality is only one small part of it.

Instead of extending Equatable, you can just have RawValue and compare it. For example:

enum E: String { 
  case A, B, C(Int)

  var rawValue: String {
    if case .C(let something) = self { return "c_\(something)" }
    return String(describing: self)
  }
}

And than you can compare:

if A.rawValue != B.rawValue { ... }