Thanks for the detailed explanation Jordan! Comment inline: > On 3 Aug 2017, at 02:08, Jordan Rose <jordan_r...@apple.com> wrote: > > David Hart recently asked on Twitter > <https://twitter.com/dhartbit/status/891766239340748800> if there was a good > way to add Decodable support to somebody else's class. The short answer is > "no, because you don't control all the subclasses", but David already > understood that and wanted to know if there was anything working to mitigate > the problem. So I decided to write up a long email about it instead. (Well, > actually I decided to write a short email and then failed at doing so.) > > The Problem > > You can add Decodable to someone else's struct today with no problems: > > extension Point: Decodable { > enum CodingKeys: String, CodingKey { > case x > case y > } > public init(from decoder: Decoder) throws { > let container = try decoder.container(keyedBy: CodingKeys.self) > let x = try container.decode(Double.self, forKey: .x) > let y = try container.decode(Double.self, forKey: .y) > self.init(x: x, y: y) > } > } > > But if Point is a (non-final) class, then this gives you a pile of errors: > > - init(from:) needs to be 'required' to satisfy a protocol requirement. > 'required' means the initializer can be invoked dynamically on subclasses. > Why is this important? Because someone might write code like this: > > func decodeMe<Result: Decodable>() -> Result { > let decoder = getDecoderFromSomewhere() > return Result(from: decoder) > } > let specialPoint: VerySpecialSubclassOfPoint = decodeMe() > > …and the compiler can't stop them, because VerySpecialSubclassOfPoint is a > Point, and Point is Decodable, and therefore VerySpecialSubclassOfPoint is > Decodable. A bit more on this later, but for now let's say that's a sensible > requirement. > > - init(from:) also has to be a 'convenience' initializer. That one makes > sense too—if you're outside the module, you can't necessarily see private > properties, and so of course you'll have to call another initializer that can. > > But once it's marked 'convenience' and 'required' we get "'required' > initializer must be declared directly in class 'Point' (not in an > extension)", and that defeats the whole purpose. Why this restriction? > > > The Semantic Reason > > The initializer is 'required', right? So all subclasses need to have access > to it. But the implementation we provided here might not make sense for all > subclasses—what if VerySpecialSubclassOfPoint doesn't have an 'init(x:y:)' > initializer? Normally, the compiler checks for this situation and makes the > subclass reimplement the 'required' initializer…but that only works if the > 'required' initializers are all known up front. So it can't allow this new > 'required' initializer to go by, because someone might try to call it > dynamically on a subclass. Here's a dynamic version of the code from above: > > func decodeDynamic(_ pointType: Point.Type) -> Point { > let decoder = getDecoderFromSomewhere() > return pointType.init(from: decoder) > } > let specialPoint = decodeDynamic(VerySpecialSubclassOfPoint.self) > > > The Implementation Reason > > 'required' initializers are like methods: they may require dynamic dispatch. > That means that they get an entry in the class's dynamic dispatch table, > commonly known as its vtable. Unlike Objective-C method tables, vtables > aren't set up to have entries arbitrarily added at run time. > > (Aside: This is one of the reasons why non-@objc methods in Swift extensions > can't be overridden; if we ever lift that restriction, it'll be by using a > separate table and a form of dispatch similar to objc_msgSend. I sent a > proposal to swift-evolution about this last year but there wasn't much > interest.)
I may have missed the proposal you sent, because I’d be quite interested about this. I hit this restriction once in a while and I really wish we could override methods in Swift extensions. > The Workaround > > Today's answer isn't wonderful, but it does work: write a wrapper struct that > conforms to Decodable instead: > > struct DecodedPoint: Decodable { > var value: Point > enum CodingKeys: String, CodingKey { > case x > case y > } > public init(from decoder: Decoder) throws { > let container = try decoder.container(keyedBy: CodingKeys.self) > let x = try container.decode(Double.self, forKey: .x) > let y = try container.decode(Double.self, forKey: .y) > self.value = Point(x: x, y: y) > } > } > > This doesn't have any of the problems with inheritance, because it only > handles the base class, Point. But it makes everywhere else a little less > convenient—instead of directly encoding or decoding Point, you have to use > the wrapper, and that means no implicitly-generated Codable implementations > either. > > I'm not going to spend more time talking about this, but it is the officially > recommended answer at the moment. You can also just have all your own types > that contain points manually decode the 'x' and 'y' values and then construct > a Point from that. > > > Future Direction: 'required' + 'final' > > One language feature we could add to make this work is a 'required' > initializer that is also 'final'. Because it's 'final', it wouldn't have to > go into the dynamic dispatch table. But because it's 'final', we have to make > sure its implementation works on all subclasses. For that to work, it would > only be allowed to call other 'required' initializers…which means you're > still stuck if the original author didn't mark anything 'required'. Still, > it's a safe, reasonable, and contained extension to our initializer model. I like this solution. One drawback: it does force users to add an extra modifier to those initialisers, increasing the complexity of an initializer model which is already quite challenging for newcomers. > Future Direction: runtime-checked convenience initializers > > In most cases you don't care about hypothetical subclasses or invoking > init(from:) on some dynamic Point type. If there was a way to mark > init(from:) as something that was always available on subclasses, but > dynamically checked to see if it was okay, we'd be good. That could take one > of two forms: > > - If 'self' is not Point itself, trap. > - If 'self' did not inherit or override all of Point's designated > initializers, trap. > > The former is pretty easy to implement but not very extensible. The latter > seems more expensive: it's information we already check in the compiler, but > we don't put it into the runtime metadata for a class, and checking it at run > time requires walking up the class hierarchy until we get to the class we > want. This is all predicated on the idea that this is rare, though. > > This is a much more intrusive change to the initializer model, and it's > turning a compile-time check into a run-time check, so I think we're less > likely to want to take this any time soon. Indeed, turning a compile-time check into a run-time check doesn’t sound very Swifty. > Future Direction: Non-inherited conformances > > All of this is only a problem because people might try to call init(from:) on > a subclass of Point. If we said that subclasses of Point weren't > automatically Decodable themselves, we'd avoid this problem. This sounds like > a terrible idea but it actually doesn't change very much in practice. > Unfortunately, it's also a very complicated and intrusive change to the Swift > protocol system, and so I don't want to spend more time on it here. > > > The Dangers of Retroactive Modeling > > Even if we magically make this all work, however, there's still one last > problem: what if two frameworks do this? Point can't conform to Decodable in > two different ways, but neither can it just pick one. (Maybe one of the > encoded formats uses "dx" and "dy" for the key names, or maybe it's encoded > with polar coordinates.) There aren't great answers to this, and it calls > into question whether the struct "solution" at the start of this message is > even sensible. Somewhat related: I have a similar problem in a project where I need two different Codable conformances for a type: one for coding/decoding from/to JSON, and another one for coding/decoding from/to a database row. The keys and formatting are not identical. The only solution around that for now is separate types, which can be sub-optimal from a performance point of view. > I'm going to bring this up on swift-evolution soon as part of the Library > Evolution discussions (there's a very similar problem if the library that > owns Point decides to make it Decodable too), but it's worth noting that the > wrapper struct solution doesn't have this problem. > > > Whew! So, that's why you can't do it. It's not a very satisfying answer, but > it's one that falls out of our compile-time safety rules for initializers. > For more information on this I suggest checking out my write-up of some of > our initialization model problems > <https://github.com/apple/swift/blob/master/docs/InitializerProblems.rst>. > And I plan to write another email like this to discuss some solutions that > are actually doable. > > Jordan > > P.S. There's a reason why Decodable uses an initializer instead of a > factory-like method on the type but I can't remember what it is right now. I > think it's something to do with having the right result type, which would > have to be either 'Any' or an associated type if it wasn't just 'Self'. (And > if it is 'Self' then it has all the same problems as an initializer and would > require extra syntax.) Itai would know for sure.
_______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution