Thanks for the detailed explanation Jordan! Comment inline:
> On 3 Aug 2017, at 02:08, Jordan Rose <[email protected]> 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
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution