Proposal sanity check: assigning a case statement to a boolean

More syntax bikeshedding: case color is .rgb(1.0, _, _).

Upside: not confusable with an operator or an assignment.
Downside: order is reversed from today's if case.
Upside: today's if case is also considered weird, and it's bad for code completion when you type left-to-right, and this would fix that too.
Downside: has nothing to do with the other is, which is even part of pattern syntax already (case is UIView:).

let flag = case enumValue is .aCase
if case enumValue is .aCase(let associatedValue) { … }
14 Likes

How about color is case .rgb, since case usually appears with the specified enum case?

14 Likes

I would personally prefer is case as a standalone token as much as for case or if case do.
It's also easier to read and cannot be mistaken for is (is accepts a type, is case would only accept a case, which is either an Enum value or an Enum constructor function, which are values and not types):

let flag = enumValue is case .aCase
if enumValue is case .aCase { ... }

Edit: @GreatApe beated me with the timing :joy:

9 Likes

That absolutely reads better as an expression :-) I'm a little worried about it doing funny things in if though:

if case .aCase(let assoc) = enumValue { // today
if enumValue is case .aCase(let assoc) { // okay
if (enumValue is case .aCase(let assoc)) == false { // error!
if enumValue is case .aCase(let assoc) == false { // um

I mean, we can make that work (basically by delaying the "can you put bindings here" check to after the "what kind of if is this" check), but it feels subtle to me in a way that the connected initial if case doesn't. Maybe that's just me, though.

For what it's worth, is case would not only accept a case; it would accept any pattern, the same as switches and ifs:

let isOnXAxis = point is case (_, 0)

I don't think this is a problem, though!

4 Likes

But would you need to be able to bind when using this new construct? Can't you always revert to if case when there is binding going on? To me the second example is a bit confusing, and at least this time it is unnecessary, right?

And why is the third one not ok?

1 Like

I would not use is case to extract associated values since the let/var keywords shouldn't be on the right side in an assignment expression. I would use something like as? case or as! case to extract payloads as written here and let enumerations gain the same benefits optionals have.

let associatedValue = enumValue as! case .aCase // force unwrapping
if let associatedValue = enumValue as? case .aCase { ... } // unwrapping

But this is far beyond the purpose of this thread.

2 Likes

Oh huh, I didn't even think of extending this to the as? / as! spectrum. I'll avoid nitpicking your examples because you're right that it's out of scope. My main thoughts about having this new syntax work with if were mostly about having related constructs look alike; it's too bad that switch and is case would look pretty similar and if case would be the odd one out. But you're right, we don't need to support that for this other part to be a good idea.

if (enumValue is case .aCase(let assoc)) == false { // error!

In this example, assoc is only valid to use if the case matches, but "if the case matches" doesn't say anything about "the body of the if will run" if it's composed with arbitrary expressions, so the compiler can't assume in general that assoc is safe to use. That's (part of) what Joe meant by "there would be no scope for those variables to live in".

I've always considered adding expression syntax for pattern-matching to be bound up with the idea of supporting something like regular expressions. And I wouldn't want the syntax to only work with a Bool result; you ought to be able to extract matched values in some way. So it's actually a rather large feature that needs to be thought through, not something we should add just because we can easily imagine its simplest form.

12 Likes

Ah ok. But how about a solution where is case is used merely to make enum cases with associated payloads "equatable" without supplying or binding a case? In other words, it would only be used as an expression:

enum G {
    case one(Int)
    case two(Int, Int)
}
...
let isOne = someValue is case .one

This is the usage I personally miss the most. My intuition says that if we ever want to bind the associated value, then we can instead use if case as today, but I may be wrong...

3 Likes

Excuse my lack of imagination, but what else could be the result of the pattern matching, except a Bool?

I've thought about this too, and the conclusion I've come to is that something that produces bindings is always going to be attached to a scope. For anything that doesn't produce bindings, a method call or operator is fine. But we don't have that for structural patterns because they're part of the language rather than the libraryβ€”we don't have a representation of a "pattern" that can be passed around at runtime.

The other thought I've had is that something like regex captures, which do both matching and binding, are extremely difficult to make syntax for that's both (reasonably) easy to read and statically type-safe. I absolutely want us to solve that problem but I don't want to block other things on it.

(This idea has long been blocked on the idea that we'd generate "implicit properties" on enums for checking and destructuring a particular case, but even that doesn't cover other sorts of pattern matching, so maybe we shouldn't let that block this idea either.)

5 Likes

Well, if it produces a binding, yes, the bound name has to have a scope. But it could also produce a result, which we could tuple together appropriately and wrap in an optional, and then the user can do whatever they like with that value. The typing rule would be very much tied to the form of the pattern, but I think that's what you'd expect.

Ideally the "regexp" syntax would have the same dual-use properties: if you used it in a case, you could bind names directly to certain matches, and otherwise you would get the matches back tupled up and wrapped in an optional.

4 Likes

Personally I quite dislike that backwards-reading case syntax:

if case .aCase = value {
    print("a case")
}

I'd love to be able to write the more natural:

if value == .aCase {
    print("a case")
}

Then the assignment looks very familiar as well:

let bool = (value == .aCase)

In all of the above, I don't care whether the enum has associated values or not, and the syntax is exactly the same either way.

To retrieve an associated value, I wish I could write something similar to this:

if value == .aCase(let associatedValue) {
    print("a case with \(associatedValue)")
}

Technically you can achieve that in your own code, with something like this:

protocol CaseEquatable {
  associatedtype RawCase: Equatable
  var rawCase: RawCase { get }
}

extension CaseEquatable {
  static func == (lhs: Self, rhs: RawCase) -> Bool {
    return lhs.rawCase == rhs
  }
  
  static func == (lhs: RawCase, rhs: Self) -> Bool {
    return lhs == rhs.rawCase
  }
  
  func isSameCase(as other: Self) -> Bool {
    return self.rawCase == other.rawCase
  }
}

Then for each enum, conform to the protocol:

enum MyEnum {
  case foo(Int)
  case bar(String)
  case baz
}

extension MyEnum: CaseEquatable {
  enum RawCase { case foo, bar, baz }
  
  var rawCase: RawCase {
    switch self {
    case .foo: return .foo
    case .bar: return .bar
    case .baz: return .baz
    }
  }
}

It’s a bit of boilerplate, but it works.

However, to avoid overloading ==, it might make more sense to use a spelling like the pattern-match operator ~=. That would work for the (Self, Self) version as well, so the isSameCase(as:) method could be removed.

1 Like

Thank you that's interesting but in my opinion is not the sort of thing that anyone should have to do. What I have written seems the more simple and natural way of expressing what I want to do here and I wish Swift would allow me to do it.

In the case of an associated value, I think it would be acceptable to force the programmer to express explicitely if he wants to ignore it, for example:

if value == .aCase(_) {
    print("a case")
}

Then you can also compare with a specific associated value:

if value == .aCase("hello") {
    print("a case with hello")
}

Writes and reads naturally. But I'm not a compiler engineer so I may be missing as to why this might be a terrible idea.

Ah that explains it.

@tclementdev wondering what would you think about if value is case .aCase it seems like that suggestion would help it to not read backwards.

This would definitely be very useful (and thank you Nevin for the workaround!) but using a straight == doesn't seem like a good idea, even if you need to explicitly ignore the payload. I mean it's not really equality, the left side is not necessarily equal to the right, it's just similar, it matches a pattern.

So I think a better solution would be something like the ~= that Nevin mentions, or a syntax like value is case .aCase. It would be even more useful if it were an expression though, so it could be used for assignments and more generally, that would be the dream scenario!

In switch statements you can already take .aCase and make it pattern matchable (using an invisible ~=) by prefixing case, so in a way the most natural syntax would be value ~= case .aCase.

2 Likes

(Sorry; just saw this thread today.)

As for the original example, this already works:

let bool = .aCase ~= value

It only stops working when you add in associated values.

I've been using the following ~= for a while, for those. It's pretty great. :smiley_cat: And definitely the right solution because of how switch works, and the existing precedent for no associated values.

Although I think that we shouldn't have to rely on my solution (i.e. a reflection-less implementation of ~= would be nice), I think all of the constituent pieces below should be in the standard library anyway (with better-enforced correctness, though this stuff is fine in practice).

func test_enum_NotEquatable() {
  enum πŸ“§ {
    case tuple(cat: String, hat: String)
    case anotherTuple(cat: String, hat: String)
    case labeled(cake: String)
    case noAssociatedValue
  }

  let tupleCase = πŸ“§.tuple(cat: "🐯", hat: "🧒")
  XCTAssertTrue(πŸ“§.tuple ~= tupleCase)
  XCTAssertFalse(πŸ“§.anotherTuple ~= tupleCase)

  XCTAssertTrue(πŸ“§.noAssociatedValue ~= .noAssociatedValue)
  XCTAssertTrue(πŸ“§.labeled ~= πŸ“§.labeled(cake: "🍰"))

  let makeTupleCase = πŸ“§.tuple
  XCTAssertFalse(makeTupleCase ~= πŸ“§.noAssociatedValue)

  switch tupleCase {
  case πŸ“§.labeled: XCTFail()
  case makeTupleCase: break
  default: XCTFail()
  }
}
func test_enum_Equatable() {
  enum πŸ“§: Equatable {
    case tuple(cat: String, hat: String)
    case anotherTuple(cat: String, hat: String)
    case labeled(cake: String)
    case noAssociatedValue
  }

  let tupleCase = πŸ“§.tuple(cat: "🐯", hat: "🧒")
  XCTAssertTrue(πŸ“§.tuple ~= tupleCase)
  XCTAssertFalse(πŸ“§.anotherTuple ~= tupleCase)

  XCTAssertTrue(πŸ“§.labeled ~= πŸ“§.labeled(cake: "🍰"))

  let makeTupleCase = πŸ“§.tuple
  XCTAssertFalse(makeTupleCase ~= πŸ“§.noAssociatedValue)

  switch tupleCase {
  case πŸ“§.labeled: XCTFail()
  case makeTupleCase: break
  default: XCTFail()
  }
}
/// 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)
  }
}
public extension Sequence {
  typealias Tuple2 = (Element, Element)

  var tuple2: Tuple2? { makeTuple2()?.tuple }

  private func makeTuple2() -> (
    tuple: Tuple2,
    getNext: () -> Element?
  )? {
    var iterator = makeIterator()
    let getNext = { iterator.next() }

    guard
      let _0 = getNext(),
      let _1 = getNext()
    else { return nil }

    return ( (_0, _1), getNext )
  }
2 Likes

@tclementdev wondering what would you think about if value is case .aCase it seems like that suggestion would help it to not read backwards.

It's better but seems overly verbose compared to if value == .aCase(_). Also I find it really distrubing that it's so different to what you would write when there is no associated value: if value is case .aCase vs. if value == .aCase

if value == .aCase(_) has the added benefit of clearly showing that the case has an associated value but it is being ignored. With if value is case .aCase you wouldn't know by just reading this whether there is an associated value being ignored or not.

I have the same complaint here. It seems overly too different to what would be written when there is no associated value, and does not convey whether an associated value exists and is being ignored.