switch or if case …

After reading in the last Natasha the Robot blog on Swift, I experimented on the use of if case to replace switch when testing a single case.


I'm not sure I fully understand the syntax and have several points to confirm.


I have an enum with associated values


enum TestEnum : someType {
case A(valueForA: Int)
case B(valueForB: Int)
}

var testEnum: TestEnum .= A(0)


The call with a switch


switch testEnum {
case .A(let retrieveValue) : doA
default : break
}

It tests that .A matches testEnum and load associated value in retrieveValue or does it declare a new .A() constant loaded with the testEnum ?


This can be replaced by if case


if case let .A(retrieveValue) = testEnum { doA) }

question : in this case, it declares a new .A() constant ?


I can also put the let inside or outside the (), and that gives the same result :


switch testEnum {
case let .A(retrieveValue) : doA
default : break
}

or

if case .A(let retrieveValue) = testEnum { doA) }


But here the syntax seems weird, because looks like a == test, but in fact it's an allocation ?

Answered by QuinceyMorris in 161618022

1. You can put the "let" inside or outside the parentheses, because you have the same choice in the "switch" case syntax. In the rare case where the enum case's associated value is a tuple, and you want a mixture of "let" and "var" bindings for different elements of the tuple, then you must of course put the "let" or "var" inside the parentheses, separately for each tuple element.


2. I don't exactly know what you mean by "a new .A() constant", but I think the answer is no. The "let" causes a binding to be made between the variable name inside the parentheses and the associated value of the enum. There is a "new" Int value being bound, not a new ".A () constant". In your example, it means "if testEnum is case A, then define a variable retrieveValue that contains the associated value from testEnum".


3. In general, I'd avoid the second style of "switch" statement, where you use a "default" case. You're better off listing all of the cases explicitly, so that if you ever add a new case "C" you'll get a compiler error telling you that your switch is not exhaustive, forcing you to make sure you handle the new case correctly.


By analogy, this would also mean avoiding "if case let" for such enums: to avoid falling through if you ever add new cases, without your being forced to re-examine the correctness of the test.


In addition, you're absolutely right that the "if case let" syntax looks weird. It's hard to remember how to code, it's hard to read, and it's totally unclear how to write some of the more complicated "case" patterns that you'd otherwise be able to use in a genuine "switch" statement. For that reason, although it's a cool concept, I'd give it a very wide berth, until and unless there's a more streamlined syntax.

Accepted Answer

1. You can put the "let" inside or outside the parentheses, because you have the same choice in the "switch" case syntax. In the rare case where the enum case's associated value is a tuple, and you want a mixture of "let" and "var" bindings for different elements of the tuple, then you must of course put the "let" or "var" inside the parentheses, separately for each tuple element.


2. I don't exactly know what you mean by "a new .A() constant", but I think the answer is no. The "let" causes a binding to be made between the variable name inside the parentheses and the associated value of the enum. There is a "new" Int value being bound, not a new ".A () constant". In your example, it means "if testEnum is case A, then define a variable retrieveValue that contains the associated value from testEnum".


3. In general, I'd avoid the second style of "switch" statement, where you use a "default" case. You're better off listing all of the cases explicitly, so that if you ever add a new case "C" you'll get a compiler error telling you that your switch is not exhaustive, forcing you to make sure you handle the new case correctly.


By analogy, this would also mean avoiding "if case let" for such enums: to avoid falling through if you ever add new cases, without your being forced to re-examine the correctness of the test.


In addition, you're absolutely right that the "if case let" syntax looks weird. It's hard to remember how to code, it's hard to read, and it's totally unclear how to write some of the more complicated "case" patterns that you'd otherwise be able to use in a genuine "switch" statement. For that reason, although it's a cool concept, I'd give it a very wide berth, until and unless there's a more streamlined syntax.

Many thanks, quite clear : the "if case" is there to bind the associated value (which is effectively what I need most often).


In my case, I often have multiple associated values in the enum, and enum hase up to 10 case;

So your advice is not to use if case let, in either form if case .A(let x) or if case let .A(x) ? It's tedious to list all the case (when there are 10) if they are unused.


For the point of putting let inside, I have another case here I get a computer warning I cannot get rid of.

This is not a switch case but just a tuple allocation :


I write :


   if var (a, b, c) = readSomething() {
          x = a
          y = b
          c += 1
     }

I get the logical compiler warning :

Variable 'a' was never mutated; consider changing to 'let' constant


but if I change var to let, I get a logical error :

Cannot assign to property: 'options' is a 'let' constant


However, I cannot put let or var inside the tuple, to write

if (let a, let b, var c) = readSomething()


Compiler does not accept as it detects "multiple statements"


How to silence the initial warning ?

>> It's tedious to list all the case (when there are 10) if they are unused.


True, so it's a question of weighing the potential for a future bug against convenience. If your code is routinely defaulting multiple cases of an enum, that may also be a code smell, indicating that the enum is not serving you well. But it's situational, and a matter of preference. There's no rule about this.


>> I cannot put let or var inside the tuple


Yes, because the syntax here is optional binding, not pattern matching. I think I would do it like this:


if let result = readSomething() {
     (let a, let b, var c) = result
     x = a
     y = b
     c += 1
}


This clarifies to someone reading the code that it's the tuple as a whole that's an optional value, not the individual elements.

Great idea.


But compiler refuses and says it needs separators … Too bad.


Here all the messages from compiler which is very vocal :


Expected expression in list of expressions

Expected ',' separator

Expected ')' in expression list

Consecutive statements on a line must be separated by ';'

Expected expression

'let' cannot appear nested inside another 'var' or 'let' pattern

'var' cannot appear nested inside another 'var' or 'let' pattern

Try this:

if let result = readSomething(),
   case (let a, let b, var c) = result
{
    x = a
    y = b
    c += 1
}

Well, except that the second clause of the 'if' is always true, which is not ideal semantically. For example, this sort-of variant of it:


let result = readSomething()!
if case (let a, let b, var c) = result
{
  x = a
  y = b
  c += 1
}


produces a warning "'if' condition is always true".


How about this:


if let (a, b, c) = readSomething()
{
  var c = c
  x = a
  y = b
  c += 1
}


which uses the common "shadowing" pattern to make c mutable inside the scope. This also emphasizes the locality of changes made to c.

I would prefer the first, except that the goal is to silence a warning (and here it would introduce another one !).


So I'll go for the second (even if I'm not very keen on this shadowing pattern), but it does the job.


BTW, it's in fact a minor compiler bug or lack of cleverness, isn't it ?

>> I'm not very keen on this shadowing pattern


That ship has sailed. 🙂 Given that Swift optionals mean you often need non-optional variables derived from the optional ones, shadowing is a better choice than similar-but-not-identical names. At least, that's what the Swift community crowd-mind has decided, and after trying both ways I have to agree with their preference.


>> BTW, it's in fact a minor compiler bug or lack of cleverness, isn't it ?


I suggest that you submit a bug report about this, because it is kind of a hole in Swift syntax (the mixed let/var tuple bindings, I mean). It's not really a very strong complaint, though, because there is something of a code-smell about 'if var' generally (as opposed to 'if let').


This is conceptually related to the Swift 2.2 change that disallowed 'var' on function parameters, forcing them to always be 'let'. It's felt to be preferable to use one variable for the immutable source of the data, and another for the computed value that results. (Or, if you want to collapse the two, shadowing is a pretty clear way of indicating you did so.)

Claude31 wrote:

It's tedious to list all the case (when there are 10) if they are unused.

QuinceyMorris wrote:

True, so it's a question of weighing the potential for a future bug against convenience.

One technique I’ve found to be useful here is to group my cases and then access the groups via a function that calls a closure. For example:

class Connection {

    enum State {
        case initialised(destination: String)
        case connecting(socket: TCPSocket)
        case connected(socket: TCPSocket)
        case closed
        func ifConnected<Result>(_ body: (socket: TCPSocket) throws -> Result) rethrows -> Result {
            switch self {
                case .initialised:
                    break
                case .connecting(let socket), .connected(let socket):
                    return try body(socket: socket)
                case .closed:
                    break
            }
        }
    }

    var state: State

    func close() {
        state.ifConnected { (socket) in
            socket.close()
        }
        self.state = .closed
    }
}

Note This is Swift 3 that compiles with Xcode 8.0b5. Furthermore, it uses the new syntax from SE-0043.

This makes it easy to extract common state from the various cases in the enum and ensure that all call sites deal with all cases.

But, as you intimated, there’s a lot of art here. I strongly suspect that, if you asked Future Quinn™ about this stuff five years from now, you’d get a very different answer (-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

The point that troubles me is that if you bifurcate your enum cases (by using default in a switch, case in an if, or grouping in your technique), you're asking for trouble if you ever add a case that causes, um, trifurcation(?).


Your technique is a little bit robust against this, because at the very least you could change the name of the 'ifConnected' method, producing compilation errors that force you to revisit the call sites.


The alternative that I was vaguely considering was a multi-level enum, where you have one enum that (in your terms) enumerates the groups, and one or more other enums that (in effect) enumerate the cases in each group. Mostly you'd switch on the top-level group enum, occasionally you might need to switch on the detail-level enums.


But I have to admit I don't exactly practice what I preach here.

this works perfectly


It is the clean solution I was looking for, with a single line, close to the original formulation and no compiler complain !


if let result = readSomething(), case (let a, let b, var c) = result {


vs original :

if var (a, b, c) = readSomething() {


Anyway, I will file a bug report.

Filed bug (improvement) report : N° 27829248


Thanks all for your advices.

switch or if case …
 
 
Q