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
How to Access Program Arguments in Swift
Retrieve Only 5 Users At a Time :Firebase [Like Instagram]
Swiftui - Passing Data from Swiftuiview to Scenekit
How to Set the Blurradius of Uiblureffectstyle.Light
Generating Resource_Bundle_Accessor, Type 'Bundle' Has No Member 'Module'
Nsimage to Nsdata as Png Swift
Xcode 9 and Xcode 10 Giving Different Results, Even with Same Swift Version
Firebase Query Ordering Not Working Properly
Understanding Swift 2.2 Selector Syntax - #Selector()
Replacing Calayer and Cabasicanimation with Skscene and Skactions
Generic Swift 4 Enum With Void Associated Type
Swift: Setting an Optional Property of a Protocol
Swift 2/iOS 9 - Libz.Dylib Not Found
Add "For In" Support to Iterate Over Swift Custom Classes
Build Error When Trying to Override an Initializer in Xcode 6.3 Beta 3