mAu mAu - 6 months ago 52
Swift Question

Generic constrained type default value

Consider the following code:

protocol JSONParserType {
associatedtype Element
}

// MARK: - Entities
struct Item {}

// MARK: - Parsers
struct OuterParser<T: JSONParserType where T.Element == Item>: JSONParserType {
typealias Element = Item
let innerParser: T

init(innerParser: T = InnerParser()) {
self.innerParser = innerParser
}
}

struct InnerParser: JSONParserType {
typealias Element = Item
}


The
OuterParser
has a child parser that should be constrained to a specific type. Unfortunately providing a default value in the initializer (or in the property definition itself) does lead to the compiler throwing a "Default argument value of type 'InnerParser' cannot be converted to type 'T'".

If I remove the default value assignment and just instantiate the
OuterParser
providing the
InnerParser
explicitly, everything is fine.

let outerParser = OuterParser(innerParser: InnerParser())


My question is what's the reason that the approach providing a default value that actually meets the constraints does not work.

Answer

The problem is that the actual type of T isn't defined by the class – it's defined by the code that uses the class. It will therefore be defined before you do anything in your class (at either instance or static level). You therefore can't assign InnerParser to T, as T has already been defined to be a given type by that point, which may well not be InnerParser.

For example, let's consider that you have another parser struct:

struct AnotherParser: JSONParserType {
    typealias Element = Item
}

and let's assume that your current code compiles. Now consider what would happen when you do this:

let parser = OuterParser<AnotherParser>()

You've defined the generic type to be AnotherParser – but the initialiser will try to assign InnerParser to your property (now of type AnotherParser). These types don't match, therefore it cannot possibly work.

Following the same logic, this implementation also won't work:

struct OuterParser<T: JSONParserType where T.Element == Item>: JSONParserType {
    typealias Element = Item
    let innerParser: T

    init() {
        self.innerParser = InnerParser()
    }

    init(innerParser: T) {
        self.innerParser = innerParser
    }
}

Unfortunately, there's no real clean solution to this problem. I think the best your best option is probably to create two factory methods for creating your OuterParser instance.

enum Parser {
    static func createParser() -> OuterParser<InnerParser> {
        return OuterParser(innerParser:InnerParser())
    }
    static func createParser<T>(innerParser:T) -> OuterParser<T> {
        return OuterParser(innerParser:innerParser)
    }
}

let innerParser = Parser.createParser() // OuterParser<InnerParser>

let anotherParser = Parser.createParser(AnotherParser()) // OuterParser<AnotherParser>

We're using an caseless enum here to avoid polluting the global namespace with extra functions.

Although this isn't very Swifty, and for that reason I would also recommend maybe rethinking your logic for how you define your parsers.

Comments