How to handle an enum passed as the concrete type to a generic?

Hey all, hope this post makes sense. Long story short, I'm wanting to pass a enum as the concrete type for a generic parameter in a struct if it conforms to a specific protocol.

Here is a quick example of what I'm attempting to do.

//This defines a NetworkResult type. Data would be passed from Networking component.
protocol NetworkResult {
  static func result(_ data: Data) -> Self
}

//The concrete type that implements the NetworkResult. The idea is this enum will grow to have several different Results available as the need grows.

enum ReturnResult: NetworkResult {
  case result(_ data: Data)
}

//This protocol will be used to define the different results a specific type can receive.
protocol ExampleResult: NetworkResult { }

//Needed to satisfy the protocol return of the Concrete type RR in Example
extension ReturnResult: ExampleResult { }

//RR here is limited to just what is defined in its own protocol for this specific type. In this case, just NetworkResult, but in the future it could handle any kind of return value
struct Example<RR: ExampleResult> {
  func handle(_ result: RR) {
    switch result {
     case let .result(data): <------- Returns compiler error
      //Handle decoding of data for a variety of types.
    }
  }
}


let example = Example<ReturnResult>()
let testData = "Hello World!".data(using: .utf8)!
example.handle(.result(testData))

The above is a quick example of what I'm hoping to do. I can successfully receive the data sent from ReturnResult.result(data), but I don't have any way to actually switch on it or extract the data.

i.e. if I don't try and switch and do print(result), it will print result(12 bytes).

At a high level, I understand that WHY it's causing the error, because the compiler can't guarantee or see that it RR is an enum, since the only expectation is that the type includes NetworkResult conformance. I'm hoping that there is some kind of way to achieve this.

Is there a way to use an enum in this situation? Is this possible to do in Swift? Any tips on how to achieve roughly the same thing?

Swift has Result type in standard library. I would recommend to use it for most of the cases where you need a result.

Its hard to understand what are you trying to achieve, but in overall it seems an overcomplicated design. Having Example as non-generic that accepts Result<Data, Error> should be enough to cover most cases and provide abstraction level.

Result isn't what I'm looking for because the amount of return values that ReturnResult can return could be infinite, but I only want relevant ones exposed to the calling struct.

e.g.

protocol NetworkResult {
  static func result(_ data: Data) -> Self
}

protocol ImageFetchResult {
  static func image(_ image: UIImage) -> Self
}

protocol StringLookupResult {
  static func loopUpResult(_ string: String) -> Self
}

extension ReturnResult: NetworkResult { }
extension ReturnResult: ImageFetchResult { }
extension ReturnResult: StringLookupResult { }

enum ReturnResult {
  case result(_ data: Data)
  case image(_ image: UIImage)
  case loopUpResult(_ string: String)
}

//Add additional return types to `ExampleResult`:
protocol ExampleResult: NetworkResult, StringLookupResult { }

//Now `ExampleResult` can handle the NetworkResult return type, and StringLookupResult return type.

So you can see that the underlying concrete type includes all possibilities, but the struct Example<RR: ExampleResult>` only "sees" the ones that its type confirms to.

You can downcast to a specific enum case type or the enum:

  enum E: Comparable {
    case a(String)
  }
  func testE() {
    let c: any Comparable = E.a("Hello")
    if case E.a(let s) = c {
      print(s)
    }
    if let e = c as? E {
      switch e {
      case .a(let s): print(s)
      }
    }
  }

See e.g., https://goshdarnifcaseletsyntax.com

More generally, there was an interesting article posted on point that seems to track your intent:

https://swiftology.io/articles/tydd-part-2/)

(related to another forum post: https://forums.swift.org/t/se-0427-noncopyable-generics/70525/146)

The gist is that rather than merely validating, you "parse" to produce new type wrapping what you learned, in this case going from Data to some other Result type, with the type capturing what you learned to avoid re-learning it. The type can be used as a wrapper, or some co-located token, but it would enable you to encode requirements in your API, making any mistake a compile-time error.

That would suggest your ExampleResult protocol should instead be your enum. Assuming you peek at the data or use out-of-band information for the type, you'd construct different enum instances for the different known types (and perhaps a fall-back for unknown), and refer to the yet-unprocessed Data (in a way that avoids copying?). If the data is in an enum associated value type, then that type has to be known to the module defining the enum.

If NetworkResult indeed just ferries Data, it's not clear it helps. Then you'd end up with no protocols, just the enum. Your input- and output-parsing could be distributed among many components even in different modules, but they would all be able to see all the enum cases, and you'd need a new release of the enum to support new types of data explicitly.

Another approach when parsing is to have different modules contribute handlers that can peek and data and either handle it or demur. Then all the validation and down-casting happens in the contributing modules, and the vectoring module is relatively oblivious. Merely handling would be a one-step dance, but you can also have multi-stage process with recognizers producing tokens used to coordinate and schedule work (e.g., buffering).

You can have the protocol expose accessors for the enum cases you're interested in exposing generically, like:

protocol ExampleResult {
  var asResult: Data? { get }
}

enum ReturnResult: ExampleResult {
  case result(_ data: Data)

  var asResult: Data? {
    if case .result(let data) = self {
      return data
    }
    return nil
  }
}

struct Example<RR: ExampleResult> {
  func handle(_ result: RR) {
    if let data = result.asResult {
      // handle decoding data here
    }
  }
}
2 Likes

To have this statically enforced, you need either to use a concrete enum with allowed types in it, or create separate methods for each one. Other options will include casting to the type. But neither of cases require generic (or solvable within it). It looks like sum type, which in Swift naturally represented by enum right now:

struct Example {
    enum Value {
        case data(Data)
        case string(String)
    }

    func handle(_ value: Value) {
    }
}

extension Result where Success == Data, Failure == Never {
    var asExampleValue: Example.Value {
        return .data(try! get())
    }
}

extension Result where Success == String, Failure == Never {
    var asExampleValue: Example.Value {
        return .string(try! get())
    }
}

let example = Example()
let testData = "Hello World!".data(using: .utf8)!
let result = Result<Data, Never>.success(testData)
example.handle(result.asExampleValue)

*I would still prefer to use Result as generic representation instead

EDIT: Actually @Joe_Groff just suggested a better version of this via generalisation

Cool. Thank you for the tip, this definitely helps.