Home > Software design >  Difference between `let case(value)` and `case(let value)` (Swift)
Difference between `let case(value)` and `case(let value)` (Swift)

Time:08-03

Lets say we have an enum with associated values:

Enum A {
    case a1(x: Int)
    case a2(y: String)
}

And some function that switches on this enum:

func doSomething(a: A) {
    
    switch a {
    case .a1(let x):
        print("a1 let \(x)")
    case let .a2(y):
        print("let a2 \(y)")
    }
}

For example:

var x = A.a1(x: 1)
var y = A.a2(y: "hello")
doSomething(a: x) // Prints a1 let 1
doSomething(a: y) // Prints let a2 hello

As you notice I can use the syntax case .a1(let x) and case let .a2(y) with no apparent difference.

So I wanted to understand: are there valid reasons to have both syntaxes, or they are purely for convenience? For example: are there a lower-level (compiler, memory) difference between those options? Are there any examples when one or the other wouldn't work as well? Thanks

CodePudding user response:

As you notice I can use the syntax case .a1(let x) and case let .a2(y) with no apparent difference.

You're right, there's very little difference between these two syntaxes — the case let syntax is for convenience.

are there valid reasons to have both syntaxes, or they are purely for convenience? For example: are there a lower-level (compiler, memory) difference between those options?

Yes, but it best applies to enum cases with more than one associated value, as it allows you to avoid having to repeat let for every single associated value in the case:

enum A {
    case a1(x: Int)
    case a2(y: String)
    case a3(x: Int, y: String)
    case a4(x: Int, y: String, z: Double)
}

let a: A = ...
switch a {
// Inner `let`:
case .a1(let x):
    print(x)

// Equivalent outer `let`:
case let .a2(y):
    print(y)

// Inner `let`s can get repetitive:
case .a3(let x, let y):
    print(x, y)

// More convenient outer `let`s:
case let .a4(x, y, z):
    print(x, y, z)
}

An explanation from the Swift language guide:

If all of the associated values for an enumeration case are extracted as constants, or if all are extracted as variables, you can place a single var or let annotation before the case name, for brevity

This is a purely syntactic convenience, and makes absolutely no difference in compilation.


If you're curious about how this shakes out from the language grammar:

  • switch statements have a list of switch cases
  • Regular cases (non-default cases) have a case label (which has one or more case items)
  • Case items are bound as patterns
  • Patterns are where the syntax branches:
    1. In the case let .a4(...) variant, the pattern is bound as a value binding pattern whose internal pattern binds as an enum case pattern
    2. In the case .a3(let...) variant, the pattern is bound directly as an enum case pattern, whose tuple pattern contents each bind a value binding pattern

The grammar technically allows for a lot of flexibility here, but the compiler ensures that you either have a var/let outside of the case label itself, or bind each variable inside of the case with a var/let internally

CodePudding user response:

This is only a syntactic difference. Consequently, x is bound to the enum case's associated value whether you write let .someCase(x) or .someCase(let x) as the pattern of the switch case.

.someCase(let x)

This is an enum case pattern, with a value-binding pattern (let x) within it. Within the value-binding pattern, there is an identifier pattern x. As an abstract syntax tree, it looks like this:

  enum case pattern
         |
value-binding pattern
         |
  identifier pattern

let .someCase(x)

In this case, the value-binding pattern is the root of the tree instead:

value-binding pattern
         |
  enum case pattern
         |
  identifier pattern

Note that the syntax for value-binding patterns is very flexible, any kind of pattern can follow the keyword let/var.

value-binding-pattern ::= 'let' pattern | 'var' pattern

According to the value-binding patterns section of the Swift guide,

Identifiers patterns within a value-binding pattern bind new named variables or constants to their matching values.

In both cases, the identifier pattern is nested within the value-binding pattern, directly or indirectly. Therefore, x is bound to the matched associated value in the same way.

It might seem like writing let first thing in the pattern is always a good idea since it binds all the identifiers to the newly matched values in one go. But sometimes you don't actually want to bind everything:

enum Foo {
    case someCase(Int, Int, Int)
}

func someFunction(someParameter: Int, foo: Foo) {
    switch foo {
    // if you wrote "let .someCase(someParameter, y, z)" here,
    // then someParameter would be bound to the associated value, rather than
    // the function's parameter!
    case .someCase(someParameter, let y, let z):
        break
    default:
        break
    }
}
  • Related