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

Reply via email to