Pattern matching: Can I match against structs?

I tried to use pattern matching. I have code like this:

struct Quantity {
   let quantity: Int
   let unit: Unit?

   init(_ quantity: Int, unit: Unit? = nil) {
      self.quantity = quantity
      self.unit = unit
   }
}

enum Unit {
   case liter
   case kilogram
   case meter
}

and I wanted to write a function that formats such a quantity nicely. I started writing it like this:

func formatQuantity(_ quantity: Quantity) -> String { }

I then wanted to switch on quantity. I tried the following syntax:

  • case Quantity(let quantity, unit: nil)
  • case let Quantity(quantity, unit: nil)
  • case Quantity(quantity: let quantity, unit: nil)

…but none of them work. Is this possible to do somehow?

It seems to interpret Quantity(quantity, unit: nil) as an expression, even with let before it, and let quantity inside an expression as not allowed.

You can do it with ~=. Search for something like "Swift pattern matching ~=".

But, it doesn't make sense for Quantity. The reason you switch on an enum is to access its associated values and/or handle multiple cases. The only thing you need to switch on here is unit.

2 Likes

Maybe what you want is tuple matching

func formatQuantity(_ quantity: Quantity) -> String {    
    switch (quantity.quantity, quantity.unit) {
    case (let q, nil): "\(q) nil"
    case (let q, .liter): "\(q) liter"
    default: "unmatched"
    }
}

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/controlflow#Tuples

1 Like

There’s nothing built in for it, because there’s no way to “reverse” a struct initializer. But you can always have a computed property return a tuple:

extension Quantity {
    var destructured: (quantity: Int, unit: Unit?) { (quantity, unit) }
}

switch quantity.destructured {
case (let quantity, nil):
    …
default:
    …
}
3 Likes

Seems reasonable to me, given that structs are more than just transparent bags of values (like e.g. non-opaque structs in C). That's what tuples are.

1 Like

You can even do this generically:

protocol Destructurable {
    func destructured<each Property>(_ key: repeat (KeyPath<Self, each Property>)) -> (repeat each Property)
}

extension Destructurable {
    func destructured<each Property>(_ key: repeat (KeyPath<Self, each Property>)) -> (repeat each Property) {
        return (repeat self[keyPath: each key])
    }
}

Allowing for this:

struct Quantity {
    var quantity: Int
    var unit: String
}

extension Quantity: Destructurable { }

func main() {
    let q = Quantity(quantity: 42, unit: "bablefish")
    switch q.destructured(\.quantity, \.unit) {
    case (42, let unit):
        print(unit)
    default:
        print("default")
    }
    // prints: bablefish
}

main()

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

18 Likes

I love this, thanks for sharing! And it even works without this, i.e. fully generically:

3 Likes

thanks for sharing!

You’re welcome.

And it even works without this

D’oh! Yeah, that was left over from my evolution from Becca’s example. I edited my post to remove it.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

Key paths don't support throwing properties :cry:, and rethrows doesn't work with parameter packs, but you can support both

q.apply(\.quantity, \.unit)

and

extension Quantity {
  enum Error: Swift.Error { }
  var property: Bool { get throws(Error) { .init() } }
}

try q.apply(
  { $0.quantity },
  { $0.unit },
  { try $0.property }
)

with

extension Destructurable {
  func apply<each Transformed, Error: Swift.Error>(
    _ transform: repeat (Self) throws(Error) -> each Transformed
  ) throws(Error) -> (repeat each Transformed) {
    try (repeat (each transform)(self))
  }
}

(This probably makes more sense as a free function because it supports more than destructuring.)

1 Like

This code doesn’t see

This code doesn’t seem to compile on my installation, even if I move it to a standalone function…

https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/swift6mode/

Thanks

This is a cool trick, but practically when is this better than switch (q.quantity, q.unit) {? You have to write out the property names in full in both cases, and obviously the pack approach has a lot more associated boilerplate (and even the call site is longer character-count wise).

7 Likes

and even the call site is longer character-count wise

Sure, if your variable name is q. With a realistic variable name, like latestQuantity, the difference starts to start.

However, I tend to agree with your larger point. I doubt I’ll adopt this technique in my own code. I was mostly just using this as an excuse to work on my variadic generic skills (-:

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

3 Likes