M-P
M-P

Reputation: 4959

Swift: Can the type of an Element in an Array be discovered and used to specify the generic type argument?

I have a protocol named APIRequest with an associated type named ResponseType and a decode function. This example is not complete, but I believe these are the only relevant parts for the question.

There's also a struct named ArrayResponse to represent when a network response returns as an array of items of different objects (depending on the specific APIRequest's ResponseType, as well as totalItems.

protocol APIRequest {
    associatedtype ResponseType: Codable

    /// Decodes the request result into the ResponseType
    func decode(_: Result<Data, APIError>) throws -> ResponseType
}

struct ArrayResponse<T>: Codable where T: Codable {
    let items: [T]
    let totalItems: Int
}

Here's an example of a struct that adheres to the APIRequest protocol and specifies it's ResponseType as Brand, which is a Codable struct that represents brand data being returned from the server.

struct BrandRequest: APIRequest {
    typealias ResponseType = Brand
}
struct Brand: Codable {
    var brandID: Int
    var brandName: String?
}

The BrandRequest is used to fetch a single Brand from the server, but I can also fetch an array of Brand's (represented by the ArrayResponse above, since Brand is one of many different types that all follow the same pattern), using BrandsRequest, which specifies it's ResponseType as an array of Brands.

struct BrandsRequest: APIRequest {
    typealias ResponseType = [Brand]
}

Rather than providing a decode function in every struct that adheres to APIRequest, I've decided to make a default implementation in a protocol extension, since they all follow the same decoding.

Depending on whether the ResponseType is an array (such as [Brand], or a single item, such as Brand, I use a different version of the decode function. This works well for the single item, but for the array of items, I'd like to look into the Array, discover the type of it's Elements, and use that to check if the result.decoded() is decoded as an ArrayResponse<> of that particular type.

So, for example, if I make a BrandsRequest, I'd like this top decode function which decodes the Array to return (try result.decoded() as ArrayResponse<Brand>).items with Brand being a different struct (e.g. Product, Customer, etc.) depending on the type of the Element in the array this function receives. This example has some non-compiling code as my attempt to get the elementType and use it as a generic argument, but of course that does not work. I also cannot simply pass Codable as the generic argument, since the compiler tells me: Value of protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Decodable'; only struct/enum/class types can conform to protocols.

So my questions are:

  1. Is there a way to capture the Type of the Element in the Array to use in ArrayResponse<insert type here>?
  2. Is there a better way to decode the network responses that return arrays of items that looks like ArrayResponse vs. single item response like Brand?
extension APIRequest where ResponseType == Array<Codable> {
    func decode(_ result: Result<Data, APIError>) throws -> ResponseType {
        let elementType = type(of: ResponseType.Element.self)
        print(elementType)

        return (try result.decoded() as ArrayResponse<elementType>).items
    }
}

extension APIRequest {
    func decode(_ result: Result<Data, APIError>) throws -> ResponseType {
        return try result.decoded() as ResponseType
    }
}

Addendum: One other approach I thought of is to change ArrayResponse<> to use T as the array type, rather than the element type:

struct ArrayResponse<T>: Codable where T: Codable {
    let items: T
    let totalItems: Int
}

and then to simplify the array decode like so:

extension APIRequest where ResponseType == Array<Codable> {
    func decode(_ result: Result<Data, APIError>) throws -> ResponseType {
        return (try result.decoded() as ArrayResponse<ResponseType>).items
    }
}

However, the compiler gives me these 2 errors: 'ArrayResponse' requires that 'Decodable & Encodable' conform to 'Encodable' and Value of protocol type 'Decodable & Encodable' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols


Addendum 2: I can get everything working and compiling, if I add another associatedtype to APIRequest to define the type of Element within the array:

protocol APIRequest {
    associatedtype ResponseType: Codable
    associatedtype ElementType: Codable

    /// Decodes the request result into the ResponseType
    func decode(_: Result<Data, APIError>) throws -> ResponseType
}

and then change my array decode function to use ElementType instead of Codable:

extension APIRequest where ResponseType == Array<ElementType> {
    func decode(_ result: Result<Data, APIError>) throws -> ResponseType {
        return (try result.decoded() as ArrayResponse<ResponseType>).items
    }
}

but then I have to supply the ElementType in each struct that conforms to APIRequest, including the single requests where it's redundant to ResponseType and not used. For the array requests, it's simply the value inside the array ResponseType, which also feels repetitive:

struct BrandRequest: APIRequest {
    typealias ResponseType = Brand
    typealias ElementType = Brand
}

struct BrandsRequest: APIRequest {
    typealias ResponseType = [Brand]
    typealias ElementType = Brand
}

The crux of my problem is that I'd like to discover the Brand type within the [Brand] array, and use it for the ArrayResponse decoding.

Upvotes: 1

Views: 858

Answers (1)

Rob Napier
Rob Napier

Reputation: 299275

I suspect this is a misuse of protocols. PATs (protocols with associated types) are all about adding more features to existing types, and it's not clear this does that. Instead, I believe you have a generics problem.

As before, you have an ArrayResponse, because that's a special thing in your API:

struct ArrayResponse<Element: Codable>: Codable {
    let items: [Element]
    let totalItems: Int
}

Now, instead of a protocol, you need a generic struct:

struct Request<Response: Codable> {
    // You need some way to fetch this, so I'm going to assume there's an URLRequest
    // hiding in here.
    let urlRequest: URLRequest

    // Decode single values
    func decode(_ result: Result<Data, APIError>) throws -> Response {
        return try JSONDecoder().decode(Response.self, from: result.get())
    }

    // Decode Arrays. This would be nice to put in a constrained extension instead of here,
    // but that's not currently possible in Swift
    func decode(_ result: Result<Data, APIError>) throws -> ArrayResponse<Response> {
        return try JSONDecoder().decode(ArrayResponse<Response>.self, from: result.get())
    }
}

And finally, you need a way to create "BrandRequest" (but really Request<Brand>):

struct Brand: Codable {
    var brandID: Int
    var brandName: String?
}

// You want "BrandRequest", but that's just a particular URLRequest for Request<Brand>.
// I'm going to make something up for the API:
extension Request where Response == Brand {
    init(brandName: String) {
        self.urlRequest = URLRequest(url: URL(string: "https://example.com/api/v1/brands/(\brandName)")!)
    }
}

That said, I'd probably adjust this and create different Request extensions that attach the correct decoder (element vs array) depending on the request. The current design, based on your protocol, forces the caller at decode time to decide if there are one or more elements, but that's known when the request is created. So I'd probably build Request more along these lines, and make Response explicitly ArrayResponse:

struct Request<Response: Codable> {
    // You need some way to fetch this, so I'm going to assume there's an URLRequest
    // hiding in here.
    let urlRequest: URLRequest
    let decoder: (Result<Data, APIError>) throws -> Response
}

(and then assign the appropriate decoder in the init)


Looking at the code you linked, yeah, that's a pretty good example of using protocols to try to recreate class inheritance. The APIRequest extension is all about creating default implementations, rather than applying generic algorithms, and that usually suggests an "inherit and override" OOP mindset. Rather than a bunch of individual structs that conform to APIRequest, I would think this would work better as a single APIRequest generic struct (as described above).

But you can still get there without rewriting all the original code. For example, you can make a generic "array" mapping:

struct ArrayRequest<Element: Codable>: APIRequest {
    typealias ResponseType = [Element]
    typealias ElementType = Element
}

typealias BrandsRequest = ArrayRequest<Brand>

And of course you could push that up a layer:

struct ElementRequest<Element: Codable>: APIRequest {
    typealias ResponseType = Element
    typealias ElementType = Element
}

typealias BrandRequest = ElementRequest<Brand>

And all your existing APIRequest stuff still works, but your syntax can be a lot simpler (and there's no actual requirement to create the typealiases; ElementRequest<Brand> is probably fine on its own).


Extending some of this based on your comment, you want to add an apiPath, and I take it you're trying to figure out where to put that information. That fits perfectly in my Request type. Each init is responsible for creating an URLRequest. Any way it wants to do that is fine.

Simplifying things to the basics:

struct Brand: Codable {
    var brandID: Int
    var brandName: String?
}

struct Request<Response: Codable> {
    let urlRequest: URLRequest
    let parser: (Data) throws -> Response
}

extension Request where Response == Brand {
    init(brandName: String) {
        self.init(
            urlRequest: URLRequest(url: URL(string: "https://example.com/api/v1/brands/\(brandName)")!),
            parser: { try JSONDecoder().decode(Brand.self, from: $0) }
        )
    }
}

But now we want to add User:

struct User: Codable {}

extension Request where Response == User {
    init(userName: String) {
        self.init(urlRequest: URLRequest(url: URL(string: "https://example.com/api/v1/users/\(userName)")!),
                  parser: { try JSONDecoder().decode(User.self, from: $0) }
        )
    }
}

That's almost identical. So identical that I cut-and-pasted it. And that tells me that it is now time to pull out reusable code (because I'm getting rid of a real duplication, not just inserting abstraction layers).

extension Request {
    init(domain: String, id: String) {
        self.init(urlRequest: URLRequest(url: URL(string: "https://example.com/api/v1/\(domain)/\(id)")!),
                  parser: { try JSONDecoder().decode(Response.self, from: $0) }
        )
    }
}

extension Request where Response == Brand {
    init(brandName: String) {
        self.init(domain: "brands", id: brandName)
    }
}

extension Request where Response == User {
    init(userName: String) {
        self.init(domain: "users", id: userName)
    }
}

But what about ArrayResponse?

extension Request {
    init<Element: Codable>(domain: String) where Response == ArrayResponse<Element> {
        self.init(urlRequest: URLRequest(url: URL(string: "https://example.com/api/v1/\(domain)")!),
                  parser: { try JSONDecoder().decode(Response.self, from: $0) }
        )
    }
}

Arg! Duplication again! Well, then fix that problem, and putting it all together:

extension Request {
    static var baseURL: URL { URL(string: "https://example.com/api/v1")! }

    init(path: String) {
        self.init(urlRequest: URLRequest(url: Request.baseURL.appendingPathComponent(path)),
                  parser: { try JSONDecoder().decode(Response.self, from: $0) })
    }

    init(domain: String, id: String) {
        self.init(path: "\(domain)/\(id)")
    }

    init<Element: Codable>(domain: String) where Response == ArrayResponse<Element> {
        self.init(path: domain)
    }
}

extension Request where Response == Brand {
    init(brandName: String) {
        self.init(domain: "brands", id: brandName)
    }
}

extension Request where Response == User {
    init(userName: String) {
        self.init(domain: "users", id: userName)
    }
}

Now this is just one of many ways to approach it. Instead of Request extension for each type, it might be nicer to have a Fetchable protocol, and put the domain there:

protocol Fetchable: Codable {
    static var domain: String { get }
}

Then you can hang the information on the model types like:

extension User: Fetchable {
    static let domain = "users"
}

extension ArrayResponse: Fetchable where T: Fetchable {
    static var domain: String { T.domain }
}

extension Request where Response: Fetchable {
    init(id: String) {
        self.init(domain: Response.domain, id: id)
    }

    init<Element: Fetchable>() where Response == ArrayResponse<Element> {
        self.init(domain: Response.domain)
    }
}

Notice that these aren't mutually exclusive. You can have both approaches at the same time because doing it this way composes. Different abstraction choices don't have to interfere with each other.

If you did that, you'd start to move towards the design from my Generic Swift talk, which is just another way to do it. That talk is about an approach to designing generic code, not a specific implementation choice.

And all without needing associated types. The way you know an associated type probably makes sense is that different conforming types implement the protocol requirements differently. For example, Array's implementation of the subscript requirement is very different than Repeated's implementation and LazySequence's implementation. If every implementation of the protocol requirements would be structurally identical, then you're probably looking at a generic struct (or possibly a class), not a protocol.

Upvotes: 2

Related Questions