> 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