Swift Optional Pattern Matching with an Associated Value

Swift's Optional pattern matching allows you to use a trailing question mark (?) instead of matching against the .some() case.

However, in the code example below, I seem to get the same results whether I use the trailing question mark on each case line or not. This behaviour only seems to present itself when using associated values.

Why is a trailing question mark not required in this instance?

import Foundation

enum Payload { 
  case string(String)
  case double(Double)
}

struct Item { 
  var payload: Payload
}

// This what I expect I have to write. 
// Optional pattern matching with a trailing '?' on each case.

func handleOptionalSelection(_ item: Item?) {
  switch item?.payload { 
    case let .string(value)?:
      print("Selected \(value)")
    
    case let .double(value)?:
      print("Selected \(value)")
    
    case .none: 
      print("Selected nil")
  }
}

// I would have expected `value` to be an Optional here, but it's not.
// Optional pattern matching without a trailing '?' on each case.

func handleSelection(_ item: Item?) {
  switch item?.payload { 
    case let .string(value):
      print("Selected \(value)")
    
    case let .double(value):
      print("Selected \(value)")
      
    case .none: 
      print("Selected nil")
  }
}

handleOptionalSelection(Item(payload: .string("Hello World")))
handleOptionalSelection(Item(payload: .double(40)))
handleOptionalSelection(nil)

handleSelection(Item(payload: .string("Hello World")))
handleSelection(Item(payload: .double(40)))
handleSelection(nil)

The output is:

Selected Hello World
Selected 40.0
Selected nil

Selected Hello World
Selected 40.0
Selected nil

Is there a reason why both implementations produce the same output?

Hi! It produces the same result because the values associated with the cases in the enumeration are not declared as optional. If they were, the values would be printed as optional unless forced unwrapping or optional binding is performed. :blush:

I know the associated value is not an Optional, but the predicate for the switch statement, item?.payload, is an Optional. If that's not relevant in this use-case, then I would have expected one of the two things to happen:

  1. A compiler warning stating that the use of the trailing ? is not required because I'm not trying to bind to an Optional.

... or ...

  1. The inclusion of case .none would not be required, but it is.

To parallel values being implicitly converted to Optional when need be, patterns can also match into Optional if they wouldn’t have the correct type otherwise. This was added to the language after the ? pattern syntax already existed, so now we have both. (I personally prefer being explicit for pattern matching, but I understand the reasoning.)

4 Likes

To respond specifically to this point, it doesn't have anything specifically to do with optional enums with associated values, or even optional enums at all:

let x: Bool? = nil

switch x {
case true?:
  fallthrough
case false?:
  print("Non-nil")
default:
  print("Nil")
}

// ...equivalent to:
switch x {
case true:
  fallthrough
case false:
  print("Non-nil")
default:
  print("Nil")
}
2 Likes

While your example is good, involving associated values does add more potential for confusion. We have the ability to either unwrap or just rename when binding switch's argument.

func handleSelection(_ item: Item?) {
  switch item?.payload {
  case _?: print("Selected payload")
  case _: print("Selected nil")
  }
}

But we don't have the ability to refer to an associated value with the same level of optionality as that argument. I don't know that such capability would be useful (how would you match the pattern of a case?), but the spelling of its bound form is not consistent with that of its wrapping enum instance. And so, this thread.

I agree, but it's that confusion that was the motivation for my original post. Binding within a case statement seems pretty straightforward, but binding an associated value within a case statement caught me by surprise.

Consider:

enum Payload { 
  case string(String)
}

var payload: Payload? = .string("Johnny")

Example 1: Without trailing ?, causes a warning (as expected).

switch payload { 
  
  // Warning:
  // Expression implicitly coerced from 'Payload?' to 'Any'
  case let n: 
    print(n)
    
  // Warning:
  // Case is already handle by a previous pattern.
  case .none: 
    print("nil")
}

Example 2: With trailing ?, no warnings (as expected).

switch payload { 
  case let p?: 
    print(p)
  
  case .none: 
    print("nil")
}

Example 3: Without trailing '?', no warnings (somewhat surprising).

switch payload { 
  case let .string(value):
    print(value)

  case .none: 
    print("nil")
}

Example 4: With trailing '?', no warnings.

switch payload { 
  case let .string(value)?:
    print(value)
  
  case .none: 
    print("nil")
}

I realize that the associated value is non-Optional, but I'm somewhat surprised that Example 3 and Example 4 product the same output without one of them showing a warning.

Or, perhaps stated another way: Is there a situation where Example 3 and Example 4 would behave differently? If not, then which of the two is the more idiomatic way to write such an expression?

You think that the you in that "your" was you, but it was Xiaodi Wu. (The internet needs better "equivalent of looking at a person, matching up with text" UI.)

With one case, it looks confusing—it's isomorphic to the associated value itself, and not having an enum case. But add another case, like in your original example. If the binding were to be allowed to be optional, reason us through how you could bind both types of value, and have that optionality be meaningful.

I say never write stuff in Swift that you don't need to, but regardless that I wouldn't use it, I don't even think that question mark should compile.

I’m sorry, I don’t understand a single word of this, nor how it serves to explain what’s going on.

@jrose’s explanation is the correct answer and I was merely pointing out that it is applicable whenever switching over optional values.

While I think you make a lot of valuable and well-informed contributions, I've never seen you have a convivial response to anything I've said, or demonstrate an understanding of it. It would probably be best if you took the common route of just not responding to me, rather than being forced to be rude.

I'm genuinely not trying to be rude; if you're going to jump into a thread and address me directly as "You" with a paragraph I can't make heads or tails of, though, I'm going to tell you (how else am I to reply?)—particularly in a scenario where others are trying to explain what's going on by distilling a problem down to its essence and have already given a complete and correct answer.

I really, truly have no idea what's going on in your post, and not for lack of trying as I've read it over multiple times and found no purchase at all; if others feel confused by it too, it's important that they know that they don't have to puzzle out what you're saying in order to feel like they've understood the topic at hand.

If you want to treat this as an invitation to expound on your statement, here are some phrases specifically that I don't understand:

  • "either unwrap or just rename when binding switch's argument"—What does this mean? Grammatically, the sentence is missing an object: unwrap or rename what?
  • [code example]—What are you trying to illustrate here with respect to the prior sentence? Nothing appears to be unwrapped or renamed?
  • "refer to an associated value"—What do you mean by "refer to"?
  • "with same level of optionality"—What does this clause modify? "Refer...with" or "associated type...with"?
  • "as that argument"—What argument? What is the relationship of the argument here with the associated value that you "refer" to?
  • "I don't know that such capability would be useful"—What capability? Useful for what?
  • "how would you match the pattern of a case?"—The case keyword is how one introduces all pattern matching in Swift, and your example shows two such examples of pattern matching a case, so what do you mean when you ask "how" to do that?
  • "spelling of its bound form"—What is "its"? Grammatically, it can only be "capability," but capabilities don't have a spelling, so that can't be it. And what you mean by "bound form"?
  • "consistent with"—What consistency of spelling do you mean? Given the construction, "I don't know that [x], but [y]," y would be read as an argument in favor of x, but perhaps not totally convincing enough to be overwhelmingly persuasive. In this case, then, why is consistency of spelling (of _____) an argument weakly in favor of the capability (to _____) being useful (for _____)?
  • "its wrapping enum instance"—Here as well, what is "its" that is wrapped?
5 Likes

@Jessy is asking why we cannot hoist associated values from optional enum values into optionals of the associated values themselves.

enum Doll
{
    case barbie(Career)
}

in essence, instead of writing:

func profession(of doll:Doll?) -> String
{
    let career:Career?
    switch doll
    {
    case .barbie(let _career)?:
        career = _career
    case nil:
        career = nil
    }
    return career?.description ?? "<unknown>"
}

it would be nice if we were able to write:

func profession(of doll:Doll?) -> String
{
    switch doll
    {
    case .barbie(let career):
        return career?.description ?? "<unknown>"
    }
}

this is a reasonable question to ask, because if the Doll type were modeled as a struct with a Career field, we could simply write:

struct Barbie
{
    let career:Career
}

func profession(of doll:Barbie?) -> String
{
    switch doll?.career
    {
    case let career:
        return career?.description ?? "<unknown>"
    }
}

or even just doll?.career.description ?? "<unknown>".

in real code bases, this often leads to forced adaptations that look like:

@_documentation(visibility: hidden)
public
enum DollType
{
    case barbie
    case bratz
    ...
}
public
struct Doll
{
    let career:Career
    let _type:DollType

    @inlinable internal
    init(_type:DollType, career:Career)
    {
        self.career = career
        self._type = _type
    }

    @inlinable static
    func barbie(_ career:Career) -> Self
    {
        ...
    }
    @inlinable static
    func bratz(_ career:Career) -> Self
    {
        ...
    }
}

in order to preserve the ability to chain on career. but this is obviously a ton of heavy boilerplate to model an enum-shaped thing using a struct.

perhaps if Jessy had used language architect-friendly words like “using product types to mimic sum types in order to workaround limitations in switch-case pattern matching is suboptimal”, this would have been clearer.

1 Like

Thanks for very clearly laying out these points. No need for “language architect” words, but very helpful to clarify that you’re describing a non-existent feature, not explaining the behavior of the language as it exists now. Let’s continue this conversation when enum keypaths are under consideration again for Swift Evolution.

1 Like