> On Mar 17, 2017, at 5:13 PM, Brent Royal-Gordon <[email protected]>
> wrote:
>
>> On Mar 17, 2017, at 2:38 PM, Matthew Johnson <[email protected]
>> <mailto:[email protected]>> 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
>
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution