> On Sep 2, 2017, at 00:30, Fabian Ehrentraud <[email protected]> 
> wrote:
> 
> 
> Am 03.08.2017 um 02:09 schrieb Jordan Rose via swift-evolution 
> <[email protected] <mailto:[email protected]>>:
> 
>> 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.)
>> 
>> 
>> 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.
>> 
>> 
>> 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.
>> 
>> 
>> 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.
>> 
>> 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
>> [email protected] <mailto:[email protected]>
>> https://lists.swift.org/mailman/listinfo/swift-evolution 
>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
> 
> I just bumped in to the required initializer problem when I tried to make my 
> ObjC classes Codable. So there really is no way to make an ObjC class Codable 
> without subclassing it in Swift or writing wrappers? Unfortunately neither is 
> economic for large amount of classes, isn't there even a hacky workaround?
> 
> Initially I had hopes that I could write an extension to the common base 
> class of my models, to conform to Codable. I currently use Mantle and I 
> thought in my Decodable init I could wrap Mantle's decoding methods.

Aha, there is actually a hacky way to handle the required initializer problem: 
rely on protocols. Swift protocol extension methods are a lot like the 
'required' + 'final' solution I described above, so you can do something like 
this:

protocol SwiftFooConstructible {
  init(swift: Foo)
}
extension SwiftFooConstructible where Self: ObjCFooConstructible {
  init(swift foo: Foo) {
    self.init(objc: foo)
  }
}

Whether or not you'll be able to use that to match Decodable up with Mantle, 
I'm not sure. But it is an option for now – and I'll stress that "for now", 
because we want to treat members that satisfy protocol requirements in classes 
as overridable by default, and that would break this.

Jordan
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to