Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom pattern matching fails with `Enum case is not a member of type`

I'm trying to write a custom pattern matching that lets me switch on an error and match against the error code. See an example below:

enum ErrorCode: Int {
  case notSoDumb
  case dumbError
}

let myError = NSError(domain: "My domain", code: ErrorCode.dumbError.rawValue, userInfo: nil)

func ~=(pattern: ErrorCode, value: NSError) -> Bool {
  return (ErrorCode(rawValue: value.code) == pattern)
}

switch myError {
case ErrorCode.notSoDumb:
  print("Not a dumb error")
case ErrorCode.dumbError:
  print("Super dumb error")
default:
  print("No matches!")
}

The first case in my switch statement has an error of Enum case 'notSoDumb' is not a member of type 'NSError'. If I replace the ErrorCode enum with integers (and update my custom ~= operator to match Ints and NSErrors, everything works fine.

like image 465
kubi Avatar asked Nov 01 '17 17:11

kubi


1 Answers

This is a known bug with enum cases in pattern matching; the compiler incorrectly assumes that it's always working with an enumeration case pattern rather than an expression pattern.

Until fixed, one way to force the compiler into 'expression pattern mode' is to first bind the case a temporary:

let notSoDumbErrorCode = ErrorCode.notSoDumb
let dumbErrorCode = ErrorCode.dumbError

switch myError {
case notSoDumbErrorCode:
  print("Not a dumb error")
case dumbErrorCode:
  print("Super dumb error")
default:
  print("No matches!")
}

However this is pretty clunky. A perhaps better workaround would be to use a struct with static members rather than an enum:

import Foundation

struct ErrorCode : Equatable, RawRepresentable {

  let rawValue: Int

  static let notSoDumb = ErrorCode(rawValue: 0)
  static let dumbError = ErrorCode(rawValue: 1)
}

let myError = NSError(domain: "My domain",
                      code: ErrorCode.dumbError.rawValue,
                      userInfo: nil)

func ~=(pattern: ErrorCode, value: NSError) -> Bool {
  return value.code == pattern.rawValue
}

switch myError {
case ErrorCode.notSoDumb:
  print("Not a dumb error")
case ErrorCode.dumbError:
  print("Super dumb error")
default:
  print("No matches!")
}

This also enables you to add additional error codes via extensions later down the line (it behaves more like an open enum). Although it does remove validation from init(rawValue:), which may or may not be desirable (you could always implement your own init?(rawValue:) though).

Or, as you say in your comment, you could stick with using an enum, but instead use an intermediate function call when pattern matching to force the compiler into 'expression pattern mode':

enum ErrorCode : Int {
  case notSoDumb
  case dumbError
}

// ...

func identity<T>(_ t: T) -> T { return t }

switch myError {
case identity(ErrorCode.notSoDumb):
  print("Not a dumb error")
case identity(ErrorCode.dumbError):
  print("Super dumb error")
default:
  print("No matches!")
}
like image 126
Hamish Avatar answered Sep 29 '22 12:09

Hamish