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

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.

Best,
Fabian

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

Reply via email to