swift, troubles with generic parameters and constraints - swift

I'm writing some tests and mocks for my product but I'm having troubles with generic parameters and constraints...
enum ApiResult<Success, Failure> where Failure: Error {
case success(Success)
case failure(Failure)
case disconnected
}
var askedDecodableApiResult: ApiResult<Decodable, Error> = .disconnected
func complete<T>(callback: #escaping (ApiResult<T, Error>) -> Void) where T: Decodable {
callback(askedDecodableApiResult) // πŸ’₯ Cannot convert value of type 'ApiResult<Decodable, Error>' to expected argument type 'ApiResult<T, Error>'
}
I'm lost with that error. What should I do to be able to send in my callback the pre-defined answer ? In the end, I want to be able to give my askedDecodableApiResult any ApiResult sporting a value that either inherits from Decodable or inherits from Error.
I need your help, thanks in advance :)

Related

Result type with generic Success Type

I'm switching over a project from a custom Result type to the native Swift Result type and keep running into this - or similar - error messages:
Member 'success' in 'Result<_, Error>' produces result of type 'Result<Success, Failure>', but context expects 'Result<_, Error>'
protocol DataFetcher {
func request<T>(completion: #escaping (Result<T, Error>) -> Void )
}
struct RandomFetcher: DataFetcher {
func request<String>(completion: #escaping (Result<String, Error>) -> Void) {
let str = "Hello"
completion(.success(str))
}
}
The idea is to have make a bunch of generic Fetchers for different data extraction calls and to pass these to VC's who would have a var fetcher: DataFetcher property. The VC's know which concrete types they expect from their request. I can't use an associated type as I need to save a bunch of these in an array and I thought I could get away with just the generic implementation - but it almost seems as if the Result type being declared as a generic in the protocol, means that it won't accept when I specify it in the implementation. What am I missing?
func request<String>(completion: #escaping (Result<String, Error>) -> Void) {
This is a classic generics mistake in Swift. This does not mean "request requires T==String." This means "there is an arbitrary type called String that this function accepts." There is no relationship between this String and Swift.String.
What you're trying to do here violates the protocol. The protocol says that the caller gets to pick any T they want. You can't conform to that protocol with a restricted T.
If you want the conforming type (RandomFetcher) to get to decide the type of T, then you have to use an associatedType (that's what associatedType means).
The thing you're trying to build is pretty common, but not trivial, and requires a different way of thinking about the problem. I walk through it step-by-step in this series.
In this case an associated type is preferable
protocol DataFetcher {
associatedtype FetchType
func request(completion: #escaping (Result<FetchType, Error>) -> Void )
}
struct RandomFetcher: DataFetcher {
func request(completion: #escaping (Result<String, Error>) -> Void) {
let str = "Hello"
completion(.success(str))
}
}

Codable Conformance with Erased Types?

I'm trying to write a generic function to parse several different data types.
Originally this method only worked for Codable types, so its generic type was constrained with <T: Codable> and all was well. Now though, I am attempting to expand it to check if the return type is Codable, and parse the data accordingly based on that check
func parse<T>(from data: Data) throws -> T? {
switch T.self {
case is Codable:
// convince the compiler that T is Codable
return try? JSONDecoder().decode(T.self, from: data)
case is [String: Any].Type:
return try JSONSerialization.jsonObject(with: data, options: []) as? T
default:
return nil
}
}
So you can see that the type-checking works fine, but I'm stuck on getting JSONDecoder().decode(:) to accept T as a Codable type once I've checked that it is. The above code doesn't compile, with the errors
Cannot convert value of type 'T' (generic parameter of instance method 'parse(from:)') to expected argument type 'T' (generic parameter of instance method 'decode(_:from:)')
and
In argument type 'T.Type', 'T' does not conform to expected type 'Decodable'
I've tried a number of casting techniques, like let decodableT: <T & Decodable> = T.self etc., but all have failed-- usually based on the fact that Decodable is a protocol and T is a concrete type.
Is it possible to (conditionally) reintroduce protocol conformance to an erased type like this? I'd appreciate any ideas you have, either for solving this approach or for similar generic-parsing approaches that might be more successful here.
EDIT: A Complication
#vadian helpfully suggested creating two parse(:) methods with different type constraints, to handle all cases with one signature. That's a great solution in many cases, and if you're stumbling across this question later it may very well solve your conundrum.
Unfortunately, in this only works if the type is known at the time that parse(:) is invoked-- and in my application this method is called by another generic method, meaning that the type is already erased and the compiler can't pick a correctly-constrained parse(:) implementation.
So, to clarify the crux of this question: is it possible to conditionally/optionally add type information (e.g. protocol conformance) back to an erased type? In other words, once a type has become <T> is there any way to cast it to <T: Decodable>?
You can conditionally cast T.self to Decodable.Type in order to get a metatype that describes an underlying Decodable conforming type:
switch T.self {
case let decodableType as Decodable.Type:
However, if we try to pass decodableType to JSONDecoder, we have a problem:
// error: Cannot invoke 'decode' with an argument list of type
// '(Decodable.Type, from: Data)'
return try? JSONDecoder().decode(decodableType, from: data)
This doesn't work because decode(_:from:) has a generic placeholder T : Decodable:
func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T
and we cannot satisfy T.Type with Decodable.Type because Decodable doesn't conform to itself.
As explored in the above linked Q&A, one workaround for this issue is to open the Decodable.Type value in order to dig out the underlying concrete type that conforms to Decodable – which we can then use to satisfy T.
This can be done with a protocol extension:
extension Decodable {
static func openedJSONDecode(
using decoder: JSONDecoder, from data: Data
) throws -> Self {
return try decoder.decode(self, from: data)
}
}
Which we can then call as:
return try? (decodableType.openedJSONDecode(using: JSONDecoder(), from: data) as! T)
You'll note that we've had to insert a force cast to T. This is due to the fact that by casting T.self to Decodable.Type we erased the fact that the metatype also describes the type T. Therefore we need to force cast in order to get this information back.
All in all, you'll want your function to look something like this:
func jsonDecode<T>(_ metatype: T.Type, from data: Data) throws -> T {
switch metatype {
case let codableType as Decodable.Type:
let decoded = try codableType.openedJSONDecode(
using: JSONDecoder(), from: data
)
// The force cast `as! T` is guaranteed to always succeed due to the fact
// that we're decoding using `metatype: T.Type`. The cast to
// `Decodable.Type` unfortunately erases this information.
return decoded as! T
case is [String: Any].Type, is [Any].Type:
let rawDecoded = try JSONSerialization.jsonObject(with: data, options: [])
guard let decoded = rawDecoded as? T else {
throw DecodingError.typeMismatch(
type(of: rawDecoded), .init(codingPath: [], debugDescription: """
Expected to decode \(metatype), found \(type(of: rawDecoded)) instead.
""")
)
}
return decoded
default:
fatalError("\(metatype) is not Decodable nor [String: Any] nor [Any]")
}
}
I have made a couple of other changes:
Changed the return type from T? to T. In general, you either want to handle errors by having the function return an optional or by having it throw – it can be quite confusing for the caller to handle both.
Added an explicit parameter for T.Type. This avoids relying on the caller to use return type inference in order to satisfy T, which IMO is a similar pattern to overloading by return type which is discouraged by the API design guidelines.
Made the default: case fatalError as it should probably be a programmer error to supply a non-decodable type.

Disambiguate a complex closure return type (foo -> _)

I'm using Alamofire's Result class. I've boiled Result down to a simple subset for presentation here.
public enum Result<Value> {
case success(Value)
case failure(Error)
}
extension Result {
/// Evaluates the specified closure when the `Result` is a failure, passing the unwrapped error as a parameter.
public func mapError<T: Error>(_ transform: (Error) -> T) -> Result {
switch self {
case .failure(let error):
return .failure(transform(error))
case .success:
return self
}
}
}
I can't get a call to mapError to compile.
I make a simple Error class and a couple of results:
class MyError: Error { }
let s: Result<Bool> = .success(true)
let f: Result<Bool> = .failure(MyError())
Now I have something to call mapError on! Maybe I'll print any error before passing it along unchanged:
f.mapError() {
print($0)
return $0
}
Here Swift tells me "error: unable to infer complex closure return type; add explicit type to disambiguate". It doesn't seem that complex to me; mapError passes an Error into the closure, and expects one (or T:Error) returned, but I try placating Swift anyway:
f.mapError() { (e: Error)->Error in
print(e)
return e
}
Now Swift says, "error: cannot convert value of type '(Error) -> Error' to expected argument type '(Error) -> _'".
What is this _ return type? And how should I be writing the closure?
(I realize I can avoid calling mapError, but whatever, I can’t seem to call it. If I really wanted to, how would I even do so?)
You cannot pass a closure of type (Error) -> Error to mapError
because a protocol does not conform to itself, i.e. Error is not
a valid T for the constraint <T: Error>.
Making mapError non-generic
public func mapError(_ transform: (Error) -> Error) -> Result
makes both of your usage examples compile.
What is this _ return type [in the error]?
This means the compiler cannot determine a type, which might give a further clue as to what's going on...

Generic parameter T could not be inferred. Factory methods

Can someone explain to me why this wouldn't work?
I have a class with factory methods like this:
public class NetworkTask<T> {
var request: URLRequest
var completionHandler: NetworkResponse<T> -> Void
init(request: URLRequest, completionHandler: NetworkResponse<T> -> Void) {
self.request = request
self.completionHandler = completionHandler
}
static func dataResponseTaskWithRequest(request: URLRequest, completionHandler: NetworkResponse<NSData> -> Void) -> NetworkTask<NSData> {
return NetworkTask<NSData>(request: request, completionHandler: completionHandler)
}
static func mappedObjectResponseTaskWithRequest<MappedType>(request: URLRequest, completionHandler: NetworkResponse<MappedType> -> Void) -> NetworkTask<MappedType> {
return NetworkTask<MappedType>(request: request, completionHandler: completionHandler)
}
}
Then, after happily knowing that it compiles, I go to create a Task like this:
let task = NetworkTask.dataResponseTaskWithRequest(URLRequest()) { (response) in
}
Nope...
Generic parameter T could not be inferred
Wait, I can clearly infer it, the method returns NetworkTask<NSData>, so T is NSData.
Ok... then, maybe like this?
let task: NetworkTask<NSData> = NetworkTask.dataResponseTaskWithRequest(URLRequest()) { (response) in
}
Nope...
Cannot invoke 'dataResponseTaskWithRequest' with an argument list of type '(URLRequest, (_) -> _)'
Ok, so maybe the other method:
let task = NetworkTask.mappedObjectResponseTaskWithRequest(URLRequest()) { (response: NetworkResponse<String>) in
}
Nope...
Cannot convert value of type '(NetworkResponse) -> ()' to expected
argument type 'NetworkResponse<_> -> Void'
I must be clearly missing something here because the compiler can't have so many errors. Does anybody have any clue?
NetworkTask<T> is the type, not NetworkTask. That is, the parameter T is on the class and everything you do with that class, also to access its class methods, require describing that type.
Even though the T is not included in the method declaration that gives you the compiler error, there is no class NetworkTask that would contain all the class methods where the type parameter is not included – imagine instead that the method is on all classes NetworkTask<T> for any value of T. This is similar to C++ where the corresponding is even called "template", meaning that your class declaration with generic type parameters is used as a template for literally compiling different classes. This is different to for instance Java where the generics syntax is just compile time sugar with type erasure (you could indeed there call the class method – only one class really exists in that case).
Here's a minimal example to demo this some more:
class A<T> {
class func foo() {
}
class func bar(t:T) -> Void {
}
}
class B {}
A.foo() // this gives an error because the type cannot be inferred.
A.bar(1) // this works fine without compiler errors as the integer literal type can be inferred there.
In the example case above, A would be fine to called for instance with:
A<IntegerLiteralType>.foo()
You should perhaps consider whether the methods in this case belong in the class that has that type parameter T, or whether they should have something else as a receiver (or indeed whether they should be free functions?).

How to express Result<Value, Error: Errortype> in a method signature

I've been trying to make an Alamofire upgrade from 2.0 to 3.0. One of the methods contains this signature:
func standardResponse(request: NSURLRequest?, response: NSHTTPURLResponse?, result: Result<AnyObject>, success: (object: AnyObject?) -> Void, failure: (error: ServerError) -> Void)
There's an error pointing at Result<AnyObject>, stating that generic type 'Result' specialized with too few type parameters (got 1, but expected 2)
Alright, so I put in 2. According to the Alamofire 3.0 migration guide, Result has changed to accommodate an extra Error: ErrorType parameter. I tried this next:
Result<AnyObject, Error>
This time the error was that Error does not conform to protocol ErrorType.
So maybe this?
Result<AnyObject, Error: ErrorType>
No cigar. Please help me understand.
It looks like the second parameter has to be an object conforming to ErrorType.
So you could for example create your own error type with an enum like this:
enum MyErrorType: ErrorType {
case SomeError
case SomeOtherError
}
Then use it the way the compiler asks:
Result<AnyObject, MyErrorType>