How do you properly use a generic type in Swift's new Result type?

Viewed 1649

I'm trying to use Swift's new Result type in such a way that the type of the .success associated value is a generic. The rather contrived sample code below works, but is there a way to simplify the type casting so that the compiler can infer the correct type for T?

enum FetchError : Error {
  case unknownKey
}

enum FetchKey {
  case getWidth
  case getName
}

func fetchValue<T>(_ key:FetchKey) -> Result<T, FetchError> {
  switch key {

  case .getName:
    // Ideally I would like to just use: return .success("Johnny Appleseed")
    return Result<String, FetchError>.success("Johnny Appleseed") as! Result<T, FetchError>

  case .getWidth:
    return Result<Double, FetchError>.success(25.0) as! Result<T, FetchError>

  @unknown default:
    return .failure(.unknownKey)
  }
}

// This explicit type declaration is also required.
let r:Result<String, FetchError> = fetchValue(.getName)
print(r)
3 Answers

To your question about the type-casting, you can definitely simplify it:

case .getName:
    return .success("Johnny Appleseed" as! T)

This is ok if asking for the wrong type should be considered a programming error (and so should crash), and the results will never come from external sources. If the data could ever come from an external source, then you should never crash in response to it being wrong.

In that case, we should model that kind of error:

enum FetchError : Error {
    case unknownKey
    case invalidType
}

Then you can have a syntax very close to what you like by adding a function to do the (possibly failing) type conversion:

func fetchValue<Value>(_ key:FetchKey) -> Result<Value, FetchError> {

    func checkType(_ value: Any) -> Result<Value, FetchError> {
        guard let value = value as? Value else { return .failure(.invalidType) }
        return .success(value)
    }

    switch key {
    case .getName: return checkType("Johnny Appleseed")
    case .getWidth: return checkType(25.0)
    @unknown default: return .failure(.unknownKey)
    }
}

That said, I'd do it this way to avoid the ugliness of required type annotations:

func fetch<Value>(_: Value.Type, forKey key: FetchKey) -> Result<Value, FetchError> { ... }

let r = fetch(String.self, forKey: .getName)

This follows the pattern of Codable.


Here's the whole solution together in one place in a few different ways:

Returning Result

enum FetchError : Error {
    case unknownKey
    case invalidType
}

enum FetchKey {
    case width
    case name
}

func fetch<Value>(_: Value.Type, forKey key: FetchKey) -> Result<Value, FetchError> {
    func checkType(_ value: Any) -> Result<Value, FetchError> {
        guard let value = value as? Value else { return .failure(.invalidType) }
        return .success(value)
    }

    switch key {
    case .name: return checkType("Johnny Appleseed")
    case .width: return checkType(25.0)
    @unknown default: return .failure(.unknownKey)
    }
}

With throws

I think this gets a little nicer if you throw rather than wrapping things into Result. It means you can more easily lift checkType into one place and gets very close to the syntax you said you wanted.

func fetch<Value>(_: Value.Type, forKey key: FetchKey) throws -> Value {
    func checkType(value: () throws -> Any) throws -> Value {
        guard let value = try value() as? Value else { throw FetchError.invalidType }
        return value
    }

    return try checkType {
        switch key {
        case .name: return "Johnny Appleseed"
        case .width: return 25.0
        @unknown default: throw FetchError.unknownKey
        }
    }
}

With Optionals

This gets a little simpler with Optional if you don't really care about the errors.

func fetch<Value>(_: Value.Type, forKey key: FetchKey) -> Value? {
    func _fetch() -> Any? {
        switch key {
        case .name: return "Johnny Appleseed"
        case .width: return 25.0
        @unknown default: return nil
        }
    }

    return _fetch() as? Value
}

The function you are trying to create has a dynamically typed generic. This isn't possible as the Swift compiler needs to know the types for each method/variable at compile time.

Suppose you have

func whatToFetch() -> FetchKey {
 // Randomly returns one of the FetchKey cases
}

let r = fetchValue(whatToFetch())

There is no way for the compiler to know the type of r before the call is made. Your example code works because your force-casts are perfectly lined up. But in a correct implementation you should not have to specify what type T is inside your generic function


In your case, it seems generics are not really needed and you could do away with protocols:


enum FetchKey {

    case someStringValueKey
    case someDoubleValueKey
    case someModelValueKey
}

protocol FetchResult {}

extension String: FetchResult {}
extension Double: FetchResult {}
extension SomeModel: FetchResult {}

func fetch(_ key: FetchKey) -> Result<FetchResult,Error> {
    switch key {
    case .someStringValueKey:
        return .success("")
    case .someDoubleValueKey:
        return .success(1.0)
    case .someModelValueKey:
        return .success(SomeModel())
    }
}

let wtf = whatToFetch()
let r = fetch(wtf)

switch r {
case .success(let value):
    switch wtf {
    case .someStringValueKey:
        guard let stringValue = value as? String else { return }
    case .someDoubleValueKey:
        guard let doubleValue = value as? Double else { return }
    case .someModelValueKey:
        guard let someModelValue = value as? SomeModel else { return }
    }
case .failure(let error):
    print("Better do more than just print on production code")
}

Trying to use an unqualified generic fetchValue<T> to do what you want probably isn’t going to work. The reason is that in a generic function, the T is specified by the caller, not the function. You’re essentially saying, “Ask fetchValue for any T you want, and it will give you a result.”

There’s missing link in the way you’ve set up your types. The red flag that’s your primary clue to this is the use of as!. That’s a sign that you are making assumptions about type relationships that you’re not telling the compiler about.

What does that mean? Well, note that with your code, let r: Result<URLConnection, FetchError> = fetchValue(.getName) also compiles — but then crashes at runtime!

One solution is to have an umbrella type that gathers all the possible result types, as Emil’s solution does. In this approach, you erase the type of the result, and ask callers to dynamically extract it. Callers have to deal with the possibility that the result might be of any type, not necessarily the one they expected. This is a common pattern when dealing with dynamically structured data such as JSON documents.

A second approach is to do what Rob suggests: let the caller specify the type, but translate an incorrect type into an error.

A third approach to solving this is to find a way to associate keys with result types in the type system, i.e. to tell the compiler getName → String, getWidth → Double. Unfortunately, AFAIK, there’s no way to do that with individual enum cases, so you’ll need to encode the keys as something other than an enum.

Here’s one way to do it:

enum FetchError : Error {
  case unknownKey
}

protocol FetchKey {
  associatedtype ValueType

  func get() -> ValueType
}

struct GetNameKey: FetchKey {
  func get() -> String {  // This line establishes the getName → String mapping
    return "Johnny Appleseed"
  }
}

struct GetWidthKey: FetchKey {
  func get() -> Double {  // This line establishes the getWidth → Double mapping
    return 25.0
  }
}

// This function signature means: “If you give fetchValue a key, it
// will give you a result _with the appropriate type_ for that key”
//
func fetchValue<K: FetchKey>(_ key: K) -> Result<K.ValueType, FetchError> {
  // The return type here is Result<K.ValueType, FetchError>, but
  // now Swift has enough into to infer it!
  return Result.success(key.get())
}

// This now works without type inference:
let r0 = fetchValue(GetNameKey())
print(r0)

// And this now (correctly) fails to compile:
// let r1: Result<Double, FetchError> = fetchValue(GetNameKey())

Which approach should you use?

  • Does each key always return a single, consistent type that you know beforehand (e.g. name is always a string)?
    • Yes: Use my approach above
    • No: If a fetch succeeds, but the type that came back isn’t the type the caller asked for, how do you want it reported?
      • As a Result.failure, and the caller can’t see the value: Use Rob’s approach
      • As a Result.success, and the caller has to figure out what type the value is: Use Emil’s approach
Related