[swift-evolution] [Proposal] Foundation Swift Archival & Serialization
Matthew Johnson
matthew at anandabits.com
Fri Mar 17 17:35:31 CDT 2017
> On Mar 17, 2017, at 5:13 PM, Brent Royal-Gordon <brent at architechies.com> wrote:
>
>> On Mar 17, 2017, at 2:38 PM, Matthew Johnson <matthew at anandabits.com <mailto:matthew at anandabits.com>> wrote:
>>
>>> At a broad level, that's a good idea. But why not provide something more precise than a bag of `Any`s here? You're in pure Swift; you have that flexibility.
>>>
>>> protocol Codable {
>>> associatedtype CodingContext = ()
>>>
>>> init<Coder: Decoder>(from decoder: Coder, with context: CodingContext) throws
>>> func encoder<Coder: Encoder>(from encoder: Coder, with context: CodingContext) throws
>>> }
>>> protocol Encoder {
>>> associatedtype CodingContext = ()
>>>
>>> func container<Key : CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key, CodingContext>
>>> …
>>> }
>>> class KeyedEncodingContainer<Key: CodingKey, CodingContext> {
>>> func encode<Value: Codable>(_ value: Value,? forKey key: Key, with context: Value.CodingContext) throws { … }
>>>
>>> // Shorthand when contexts are the same:
>>> func encode<Value: Codable>(_ value: Value,? forKey key: Key) throws
>>> where Value.CodingContext == CodingContext
>>> { … }
>>>
>>> …
>>> }
>>
>> This is sort of similar to the design I suggested for contexts. The difference is that you’re requiring all Codable to be context aware and by introducing an associated type you break the ability to use Codable as an existential.
>
> I don't think banning existentials is actually a loss. Since `encode(_:)` doesn't record type information, and instead `decode(_:)` requires the exact concrete type to be passed in, `Codable` existentials cannot be usefully encoded or decoded. For instance, a heterogeneous `[Codable]` would encode in several different, probably mutually incompatible formats, without any type information that could distinguish between them. Since the only semantics of `Codable` are encoding and decoding, and decoding is always done by an `init`, `Codable` existentials are useless and we lose nothing by not supporting them.
That’s fair. But how would you change the design of the NSKeyedArchiver / NSKeyedUnarchiver extensions which use the existentials?
>
>> Many Codable conforming types won’t need to know anything about a context. I would still want to be able to encode them along with my custom context-aware types. A good example is types from Foundation that will conform to Codable. They will definitely not know anything about my context but I still want to be able to encode a URL alongside my custom context-aware types.
>
> Sure; you can do that by calling `encode(_:forKey:with:)` and passing a freshly-made `()` context. We might even add a second convenience overload of `encode(_:forKey:)`:
>
> class KeyedEncodingContainer<Key: CodingKey, CodingContext> {
> func encode<Value: Codable>(_ value: Value,? forKey key: Key, with context: Value.CodingContext) throws { … }
>
> // Shorthand when contexts are the same:
> func encode<Value: Codable>(_ value: Value,? forKey key: Key) throws
> where Value.CodingContext == CodingContext
> {
> try encode(value, forKey: key, with: currentContext)
> }
>
> // Shorthand when the type uses a Void context:
> func encode<Value: Codable>(_ value: Value,? forKey key: Key) throws
> where Value.CodingContext == Void
> {
> try encode(value, forKey: key, with: ())
> }
>
> …
> }
>
> The main disadvantage I can think of in this design is that even `Codable` users who don't need a context have to have a `with context: Void` in their code. This might be confusing to new developers, but I think it's worth it.
>
> (I don't think I mentioned this anywhere, but containers like `Array` should take on the `CodingContext` of their `Element`s and pass the context they receive through without examining it. That would probably be pretty common with generic container types.)
You’re right - I just wasn’t thinking about this clearly. I missed that you were requiring Codable types to manually thread the context through. This is kind of unfortunate when *all* types involved in the encoding either have a Void context or use the same context type. On the other hand, it is a somewhat rarely needed feature and this approach offers a lot of flexibility. I think I like it.
>
>> Did you take a look at the design I suggested? What do you think of it?
>
> I think that, if a type wants to support context-free coding, it should use an optional `CodingContext`. :^)
>
> In all seriousness, I see the design as very slightly weak, in that it makes it easy to forget to pass a context through, but quite acceptable.
Easy for who? I was not requiring Codable types to thread it through at all. The context was fully managed by the Encoder / Decoder type. The only place Codable types work with the context is as an argument they receive. They never pass it when encoding or decoding anything. The Encoder / Decoder would need to store the context internally and when call is made to encode / decode a ContextAwareCodable it would pass the result of a dynamic cast to ContextAwareCodable.Context as the context.
This design encapsulates the context more completely and solves all the real world use cases I know of at the expense of some flexibility. It also guarantees that *all* Codables in a single encoding / decoding see exactly the same context or no context at all. This could be viewed as an advantage or a disadvantage.
Maybe your approach of making the context more exposed but also offering more flexibility and guaranteeing a Codable always gets the context it needs is better. I need more time to think about it, but I think it makes better tradeoffs.
> It would certainly solve the `with context: Void` problem I mentioned. I might consider reversing the relationship between the two protocols, though:
>
> public protocol ContextAwareCodable {
> associatedtype CodingContext
>
> init(from decoder: Decoder, with context: CodingContext) throws
> func encode(to encoder: Encoder, with context: CodingContext) throws
> }
> public protocol Codable: ContextAwareCodable where CodingContext == Void {
> init(from decoder: Decoder) throws
> func encode(to encoder: Encoder) throws
> }
> extension Codable {
> public init(from decoder: Decoder, with context: Void) throws {
> try self.init(from: decoder)
> }
> func encode(to encoder: Encoder, with context: Void) throws {
> try encode(to: encoder)
> }
> }
>
> Most `Encoder`/`Decoder` APIs would have to use `ContextAwareCodable`, but if you're writing a coder, you'd better be aware of contexts.
The reason I did it the other way is to allow Codable to be used as an existential. It is used that way in the NSKeyedArchiver / NSKeyedUnarchiver extensions and I wanted to find something workable that wouldn’t break that. If we aren’t worried about existentials then this would work.
Agree - the design priority should be for users and authors of Codable types. Encoders and Decoders are comparatively rare and written by people who should know what they are doing.
>
> * * *
>
> A thought I just had: Someone upthread mentioned that `Codable` might be better as part of the standard library. One reason to favor that approach is that you could then make `Codable` support a requirement of types like `BinaryInteger` and `FloatingPoint`.
>
> It might still make sense to have the coders themselves be part of Foundation; only the protocols defining `Codable`, `Encoder`, `Decoder`, and their ancillary types would be part of the standard library.
+1 to putting the protocols in the standard library and keeping the concrete encoders and decoders in Foundation.
>
> --
> Brent Royal-Gordon
> Architechies
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170317/40c851ba/attachment-0001.html>
More information about the swift-evolution
mailing list