> On Aug 6, 2017, at 12:58 PM, Charles Srstka <cocoa...@charlessoft.com> wrote:
> 
>> On Aug 3, 2017, at 12:05 PM, Itai Ferber via swift-evolution 
>> <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>> 
>> Thanks for putting these thoughts together, Jordan! Some additional comments 
>> inline.
>> 
>>> On Aug 2, 2017, at 5:08 PM, Jordan Rose <jordan_r...@apple.com 
>>> <mailto: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.)
>>> 
>>> 
>>> 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.
>> I would actually take this a step further and recommend that any time you 
>> intend to extend someone else’s type with Encodable or Decodable, you should 
>> almost certainly write a wrapper struct for it instead, unless you have 
>> reasonable guarantees that the type will never attempt to conform to these 
>> protocols on its own.
>> 
>> This might sound extreme (and inconvenient), but Jordan mentions the issue 
>> here below in The Dangers of Retroactive Modeling. Any time you conform a 
>> type which does not belong to you to a protocol, you make a decision about 
>> its behavior where you might not necessarily have the "right" to — if the 
>> type later adds conformance to the protocol itself (e.g. in a library 
>> update), your code will no longer compile, and you’ll have to remove your 
>> own conformance. In most cases, that’s fine, e.g., there’s not much harm 
>> done in dropping your custom Equatable conformance on some type if it starts 
>> adopting it on its own. The real risk with Encodable and Decodable is that 
>> unless you don’t care about backwards/forwards compatibility, the 
>> implementations of these conformances are forever.
>> 
>> Using Point here as an example, it’s not unreasonable for Point to 
>> eventually get updated to conform to Codable. It’s also not unreasonable for 
>> the implementation of Point to adopt the default conformance, i.e., get 
>> encoded as {"x": …, "y": …}. This form might not be the most compact, but it 
>> leaves room for expansion (e.g. if Point adds a z field, which might also be 
>> reasonable, considering the type doesn’t belong to you). If you update your 
>> library dependency with the new Point class and have to drop the conformance 
>> you added to it directly, you’ve introduced a backwards and forwards 
>> compatibility concern: all new versions of your app now encode and decode a 
>> new archive format, which now requires migration. Unless you don’t care 
>> about other versions of your app, you’ll have to deal with this:
>> Old versions of your app which users may have on their devices cannot read 
>> archives with this new format
>> New versions of your app cannot read archives with the old format
>> 
>> Unless you don’t care for some reason, you will now have to write the 
>> wrapper struct, to either
>> Have new versions of your app attempt to read old archive versions and 
>> migrate them forward (leaving old app versions in the dust), or
>> Write all new archives with the old format so old app versions can still 
>> read archives written with newer app versions, and vice versa
>> 
>> Either way, you’ll need to write some wrapper to handle this; it’s 
>> significantly safer to do that work up front on a type which you do control 
>> (and safely allow Point to change out underneath you transparently), rather 
>> than potentially end up between a rock and a hard place later on because a 
>> type you don’t own changes out from under you.
> 
> I should point out that there’s a third case: the case where you want to add 
> conformance to a type from another framework, but you own both frameworks.
> 
> Plenty of examples of this can be found in the Cocoa frameworks, actually. 
> For example, as NSString is, of course, declared in Foundation, its original 
> declaration cannot conform to the NSPasteboardReading protocol, which is 
> declared in AppKit. As a result, Apple declares NSString’s 
> NSPasteboardReading support in a category in AppKit. There are reasons one 
> might want to do the same thing in their own code—make one library and/or 
> framework for use with Foundation-only programs, and extend a type from that 
> library/framework with NSPasteboardReading support in a separate framework. 
> It can’t currently be done with Swift, though.
> 
> Charles
Yes, you’re right; this is something we need to do in some cases. For Codable 
specifically, I don’t think this design pattern would affect much, since:
Encoded representations of values almost overwhelmingly only need to encode 
stored properties; computed properties are very rarely part of encoded state 
(what do you do with a computed property on decode?)
Extensions in Swift cannot add storage, so you can’t extend your own types 
elsewhere with new properties that would need to encoded

I think I’d be hard-pressed to find a case where it’s not possible to adopt 
Codable on a type inside the framework it’s defined within due to a needed 
extension provided in a different framework (especially so considering Codable 
comes from the stdlib, so it’s not like there are compatibility/platform 
concerns there).

Regardless, if you own the type completely, then of course it’s safe to extend 
and control your own Codable implementation, and a struct wrapper is 
unnecessary.

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to