Generic Swift 4 Enum With Void Associated Type

Generic Swift 4 enum with Void associated type

In Swift 3 you can omit the associated value of type Void:

let res: Result<Void> = .success()

In Swift 4 you have to pass an associated value of type Void:

let res: Result<Void> = .success(())
// Or just:
let res = Result.success(())

enums with Associated Values + generics + protocol with associatedtype

I'm trying to make my API Service as generic as possible:

First, and most importantly, this should never be a goal. Instead, you should start with use cases, and make sure that your API Service meets them. "As generic as possible" doesn't mean anything, and only will get you into type nightmares as you add "generic features" to things, which is not the same thing as being generally useful to many use cases. What callers require this flexibility? Start with the callers, and the protocols will follow.

func send<T>(request: RestRequest) -> T

Next, this is a very bad signature. You don't want type inference on return types. It's a nightmare to manage. Instead, the standard way to do this in Swift is:

func send<ResultType>(request: RestRequest, returning: ResultType.type) -> ResultType

By passing the expected result type as a parameter, you get rid of the type inference headaches. The headache looks like this:

let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))

How is the compiler to know that stringResponse is supposed to be a String? Nothing here says "String." So instead you have to do this:

let stringResponse: String = ...

And that's very ugly Swift. Instead you probably want (but not really):

let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")),
returning: String.self)

"But not really" because there's no way to implement this well. How can send know how to translate "whatever response I get" into "an unknown type that happens to be called String?" What would that do?

protocol Parseable {
associatedtype ResponseType
func parse() -> ResponseType
}

This PAT (protocol w/ associated type) doesn't really make sense. It says something is parseable if an instance of it can return a ResponseType. But that would be a parser not "something that can be parsed."

For something that can be parsed, you want an init that can take some input and create itself. The best for that is Codable usually, but you could make your own, such as:

protocol Parseable {
init(parsing data: Data) throws
}

But I'd lean towards Codable, or just passing the parsing function (see below).

enum RestRequest {}

This is probably a bad use of enum, especially if what you're looking for is general usability. Every new RestRequest will require updating parse, which is the wrong place for this kind of code. Enums make it easy to add new "things that all instances implement" but hard to add "new kinds of instances." Structs (+ protocols) are the opposite. They make it easy to add new kinds of the protocol, but hard to add new protocol requirements. Requests, especially in a generic system, are the latter kind. You want to add new requests all the time. Enums make that hard.

Is there a better still clean way to achieve this?

It depends on what "this" is. What does your calling code look like? Where does your current system create code duplication that you want to eliminate? What are your use cases? There is no such thing as "as generic as possible." There are just systems that can adapt to use cases along axes they were prepared to handle. Different configuration axes lead to different kinds of polymorphism, and have different trade-offs.

What do you want your calling code to look like?

Just to provide an example of what this might look like, though, it'd be something like this.

final class ApiService {
let urlSession: URLSession
init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}

func send<Response: Decodable>(request: URLRequest,
returning: Response.Type,
completion: @escaping (Response?) -> Void) {
urlSession.dataTask(with: request) { (data, response, error) in
if let error = error {
// Log your error
completion(nil)
return
}

if let data = data {
let result = try? JSONDecoder().decode(Response.self, from: data)
// Probably check for nil here and log an error
completion(result)
return
}
// Probably log an error
completion(nil)
}
}
}

This is very generic, and can apply to numerous kinds of use cases (though this particular form is very primitive). You may find it doesn't apply to all your use cases, so you'd begin to expand on it. For example, maybe you don't like using Decodable here. You want a more generic parser. That's fine, make the parser configurable:

func send<Response>(request: URLRequest,
returning: Response.Type,
parsedBy: @escaping (Data) -> Response?,
completion: @escaping (Response?) -> Void) {

urlSession.dataTask(with: request) { (data, response, error) in
if let error = error {
// Log your error
completion(nil)
return
}

if let data = data {
let result = parsedBy(data)
// Probably check for nil here and log an error
completion(result)
return
}
// Probably log an error
completion(nil)
}
}

Maybe you want both approaches. That's fine, build one on top of the other:

func send<Response: Decodable>(request: URLRequest,
returning: Response.Type,
completion: @escaping (Response?) -> Void) {
send(request: request,
returning: returning,
parsedBy: { try? JSONDecoder().decode(Response.self, from: $0) },
completion: completion)
}

If you're looking for even more on this topic, you may be interested in "Beyond Crusty" which includes a worked-out example of tying together parsers of the kind you're discussing. It's a bit dated, and Swift protocols are more powerful now, but the basic message is unchanged and the foundation of things like parsedBy in this example.

What's the exact limitation on generic associated values in Swift enums?

This answer is out of date in Swift 2. Please see rickster's answer for Swift 2 updates.

Your comments are correct. You can't have multiple cases with associated data if any of them have unknown size. Value types could be any size (since they're copied). Reference types (like objects) have a known size, because they store a pointer.

The typical solution to this is to create an extra wrapper class to hold the generic type, as the FP book does. Everyone calls it Box by convention. There's reason to hope that the Swift team will fix this in the future. As you note, they refer to it as "unimplemented" not "unsupported."

A typical implementation of Box:

final public class Box<T> {
public let unbox: T
public init(_ value: T) { self.unbox = value }
}

Can a function argument's type be a specific enum 'case'?

You can only pass an Enum as a parameter, not its cases. So you can either write that function like this -

enum MyTimer {
case describe(userId: String,
endTime: Int)
case vote(endTime: Int)
}


func onDescribeTimerChange(timer: MyTimer) {
switch timer {
case let .describe(userId, endTime):
print(userId, endTime)
case .vote(let endTime):
print(endTime)
}
}


onDescribeTimerChange(timer: .describe(userId: "userID", endTime: 2))

Or you can use that as a method associated with your MyTimer enum type, like this -

enum MyTimer {
case describe(userId: String,
endTime: Int)
case vote(endTime: Int)

func onDescribeTimerChange() {
switch self {
case let .describe(userId, endTime):
print(userId, endTime)
case .vote(let endTime):
print(endTime)
}
}
}


MyTimer.describe(userId: "userID", endTime: 1).onDescribeTimerChange()

Extend a type with associated Types that have generic associated types

What you are after is "generic" extensions, which is a feature that is proposed and being worked on at the moment.

In the proposed syntax, you would do:

extension<S, F: Error> Array where Element == Result<S, F> {

typealias StrategiesAction = ([S]) -> Void
typealias ErrorsAction = ([F]) -> Void

func success(
_ successAction: StrategiesAction,
errors errorsAction: ErrorsAction
) {
errorsAction(compactMap { $0.failure })
successAction(compactMap { $0.success })
}
}

But you can't do exactly that right now, since the feature is still WIP. There is a work around though. Since methods can be generic, (at least in this case) you can add the generic parameters to the method instead, rather than on the extension.

extension Array {

func success<S, F: Error>(
_ successAction: ([S]) -> Void,
errors errorsAction: ([F]) -> Void
) where Element == Result<S, F>
{
errorsAction(compactMap { $0.failure })
successAction(compactMap { $0.success })
}
}

extension Result {
var failure: Failure? {
if case .failure(let f) = self {
return f
} else {
return nil
}
}

var success: Success? {
try? get()
}
}

However, the type aliases can't be generic, so you have to live without them.

This is only a work around, and not truly a "generic" extension, because success is now a function on all arrays. It is just that for arrays of non-Result, its generic constraints are not satisfied, so you can't use it.



Related Topics



Leave a reply



Submit