Decodable failing with concrete protocol metatypes?


(Bruno Rocha) #1

Consider the following models for decoding an AB testing structure:

protocol Experiment: Decodable {
    static var identifier: String { get }
}

struct HomeExperiment: Experiment {
    static let identifier: String = "home"
    let title: String
}

typealias ExperimentData = (identifier: String, data: Data)

Given that, we'll store a list of "active" experiments and decode a specific one from a given data object:

var activeExperiments: [Experiment.Type] {
    return [HomeExperiment.self]
}

static func generateExperiment(fromData data: ExperimentData) -> Experiment? {
    guard let experimentType = activeExperiments.first(where: { $0.identifier == data.identifier }) else {
        return nil
    }
    do {
        let object = try JSONDecoder().decode(experimentType, from: data.data)
        return object
    } catch {
        return nil
    }
}

I expected this to work because experimentType refers to a concrete implementation of a Decodable type, but it actually fails to compile with a Cannot invoke 'decode' with an argument list of type '(Experiment.Type, from: Data)' error.

The solution I found here was to provide a custom Decodable init that acts directly on Self:

protocol Experiment: Decodable {
    static var identifier: String { get }
}

extension Experiment {
    static func decode(fromData data: Data) throws -> Self? {
        do {
            let object = try JSONDecoder().decode(Self.self, from: data.data)
            return object
        } catch {
            return nil
        }
    }
}

///

do {
    return try experimentType.decode(fromData: data.data)
} catch {
    return nil
}

Have I misunderstood something? Why the first example fails to compile if experimentType refers to a concrete type?


(Kaitlin Mahar) #2

Since you've defined activeExperiments as having type [Experiment.Type], the concrete type is not accessible and all the compiler knows is that it's Experiment.Type, which isn't Decodable itself.

It should work if you change activeExperiments to:

var activeExperiments: [HomeExperiment.Type] {
	return [HomeExperiment.self]
}

Whether that works for what you're trying to do here, I'm not sure.


(Bruno Rocha) #3

Ah sorry for that, it works for this specific case but I could have multiple active experiments of different types, so the [Experiment.Type] abstraction is needed here.

My understanding is that (protocol).Type refers to the metatype of a type that implements the protocol, and not the protocol itself (which is (protocol).Protocol) - so it should refer to a Decodable type in the same way as HomeExperiment.self does. Have I got that wrong?


(Happy Human Pointer) #4

You want to list the Decodable methods you want to use in the Experiment protocol:

protocol Experiment: Decodable {
  // stuff 
  init(from decoder: Decoder) throws
}

(Xiaodi Wu) #5

There is no need to redeclare methods from a protocol that you're refining, and it won't do anything to solve the question at hand.


(Kaitlin Mahar) #6

My understanding is that (protocol).Type refers to the metatype of a type that implements the protocol, and not the protocol itself (which is (protocol).Protocol ) - so it should refer to a Decodable type in the same way as HomeExperiment.self does

Hm, I'm not really familiar with protocol metatypes, though based on the docs that does sound correct. Info on protocol metatypes seems pretty sparse... your blog post was the best thing I found via Google :stuck_out_tongue:


(Itai Ferber) #7

CC @jrose/@Joe_Groff/@Slava_Pestov who can likely answer this question. I'm not sure if there's currently a distinction between Experiment.Type as a type constraint (e.g. [Experiment.Type]) vs. as a value, but at the very least, you can see the following when trying to pass Experiment.Type in to decode(...) directly:

let object = try JSONDecoder().decode(Experiment.Type, from: data.data)
// error: in argument type 'Experiment.Type.Protocol', 'Experiment.Type' does not conform to expected type 'Decodable'
//        let object = try JSONDecoder().decode(Experiment.Type, from: data.data)

Perhaps my understanding of what's going on here is wrong (or perhaps the diagnostic is misleading); in any case, this seems like a place where compile-time generics are not playing nicely with runtime metatypes, and I think this is worthy of a JIRA.

In the meantime, your alternative design is sound — declaring a static method shared among all Experiment.Types callable on the metatype directly will give you what you want.


(Slava Pestov) #8

Note that Experiment.Type is really an error, but the compiler interprets it as Experiment.Type.self, which has type Experiment.Type.Type. So the diagnostic is misleading.