0

Let's say I have this enum:

enum Item {
  case foo(String)
  case bar(String)
}

and a list of it:

let items: [Item] = [.foo("aa"), .bar("bb")]

I'd like to find the first foo item of it, this is what I did:

items.first { (item) -> Bool in
  switch item {
  case .foo:
    return true
  default:
    return false
  }
}

Is there any elegant way to write something like:

items.first { case .foo = $0 }

(Above code not working, apparently)

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
X. W
  • 331
  • 2
  • 14
  • Do you need a foo/bar with an specific string value or just any foo case? Do you need to return the string value associated with it? – Leo Dabus Nov 12 '20 at 16:52
  • Hi Leo, I just need any foo case, thank you. – X. W Nov 13 '20 at 08:55
  • Possible duplicate of [How to compare enum with associated values by ignoring its associated value in Swift?](https://stackoverflow.com/questions/31548855/how-to-compare-enum-with-associated-values-by-ignoring-its-associated-value-in-s) – Cristik Nov 16 '20 at 16:38

2 Answers2

4

Enumerators with associated values (edited question)

If you have an enum whose enumerators have associated values, you could replace the switch statement if the if case (let) ... else statement, as you are exclusively searching for a given enumerator pattern (and none of the rest -> else). E.g.:

let item = items.first { 
    if case .foo = $0 { return true }
    else { return false }
}

Enumerators without associated values (original question)

= is assignment, whereas == is equality comparison. Here, you want the latter:

let item = items.first { $0 == .foo }

// or
let item = items.first { .foo == $0 }

You can also use pattern matching using the ~= operator:

let item = items.first { $0 ~= .foo }

// or
let item = items.first { .foo ~= $0 }
dfrib
  • 70,367
  • 12
  • 127
  • 192
  • Hi, thanks for your reply, I've updated the question to make it more clear as it's an enum without an associated value. – X. W Nov 12 '20 at 16:45
  • @dfrib OP solution should work as well. It is not clear what OP is trying to accomplish. Maybe OP wants to return the string value. – Leo Dabus Nov 12 '20 at 17:02
  • @LeoDabus True; I always favoured `if case let ... else` over `switch - case - default` for searching for a single enumerator, but I agree OP probably did not state exactly what was the actual intention. My first Swift answer in years! I'm surprised we aren't allowed to use pattern matching directly (`... first { .foo ~= $0 }`); was that never a thing for enumerators with associated values? I guess we'd need to implement our custom `~=` overload for the `Item` type? – dfrib Nov 12 '20 at 17:11
  • 1
    Welcome back buddy. I've never played with a collection of associated values – Leo Dabus Nov 12 '20 at 17:14
  • 1
    `if let item = items.first(where: {` `switch $0 {` `case .foo: return true` `default: return false }}), case let .foo(string) = item {` `print(string)` `}`. Arghhh Thats why I don't like enumerations with associated values. – Leo Dabus Nov 12 '20 at 18:31
  • You can use a generalized solution, like I have in my answer. There's been talk on the Swift forum of improving it without relying on reflection and extensions. –  Nov 13 '20 at 07:54
3

first won't do anything useful. I think you want contains.

items.contains { Item.foo ~= $0 }

Regardless, you'll need this:

/// Match `enum` cases with associated values, while disregarding the values themselves.
/// - Parameter case: Looks like `Enum.case`.
public func ~= <Enum: Equatable, AssociatedValue>(
  case: (AssociatedValue) -> Enum,
  instance: Enum
) -> Bool {
  Mirror.associatedValue(of: instance, ifCase: `case`) != nil
}

/// Match `enum` cases with associated values, while disregarding the values themselves.
/// - Parameter case: Looks like `Enum.case`.
public func ~= <Enum, AssociatedValue>(
  case: (AssociatedValue) -> Enum,
  instance: Enum
) -> Bool {
  Mirror.associatedValue(of: instance, ifCase: `case`) != nil
}

/// Match non-`Equatable` `enum` cases without associated values.
public func ~= <Enum>(pattern: Enum, instance: Enum) -> Bool {
  guard (
    [pattern, instance].allSatisfy {
      let mirror = Mirror(reflecting: $0)
      return
        mirror.displayStyle == .enum
        && mirror.children.isEmpty
    }
  ) else { return false }

  return .equate(pattern, to: instance) { "\($0)" }
}
public extension Mirror {
  /// Get an `enum` case's `associatedValue`.
  static func associatedValue<AssociatedValue>(
    of subject: Any,
    _: AssociatedValue.Type = AssociatedValue.self
  ) -> AssociatedValue? {
    guard let childValue = Self(reflecting: subject).children.first?.value
    else { return nil }

    if let associatedValue = childValue as? AssociatedValue {
      return associatedValue
    }

    let labeledAssociatedValue = Self(reflecting: childValue).children.first
    return labeledAssociatedValue?.value as? AssociatedValue
  }

  /// Get an `enum` case's `associatedValue`.
  /// - Parameter case: Looks like `Enum.case`.
  static func associatedValue<Enum: Equatable, AssociatedValue>(
    of instance: Enum,
    ifCase case: (AssociatedValue) throws -> Enum
  ) rethrows -> AssociatedValue? {
    try associatedValue(of: instance)
      .filter { try `case`($0) == instance }
  }

  /// Get an `enum` case's `associatedValue`.
  /// - Parameter case: Looks like `Enum.case`.
  static func associatedValue<Enum, AssociatedValue>(
    of instance: Enum,
    ifCase case: (AssociatedValue) throws -> Enum
  ) rethrows -> AssociatedValue? {
    try associatedValue(of: instance).filter {
      .equate(try `case`($0), to: instance) {
        Self(reflecting: $0).children.first?.label
      }
    }
  }
}
public extension Optional {
  /// Transform `.some` into `.none`, if a condition fails.
  /// - Parameters:
  ///   - isSome: The condition that will result in `nil`, when evaluated to `false`.
  func filter(_ isSome: (Wrapped) throws -> Bool) rethrows -> Self {
    try flatMap { try isSome($0) ? $0 : nil }
  }
}
public extension Equatable {
  /// Equate two values using a closure.
   static func equate<Wrapped, Equatable: Swift.Equatable>(
    _ optional0: Wrapped?, to optional1: Wrapped?,
    using transform: (Wrapped) throws -> Equatable
  ) rethrows -> Bool {
    try optional0.map(transform) == optional1.map(transform)
  }
}