Hi David,

> On Apr 4, 2017, at 10:33 PM, David Hart via swift-evolution 
> <[email protected]> wrote:
> 
> Very interesting discussion below. Here are a few more points:
> 
> Sent from my iPhone
> On 4 Apr 2017, at 23:43, Itai Ferber via swift-evolution 
> <[email protected] <mailto:[email protected]>> wrote:
> 
>> Hi Brent,
>> 
>> Thanks for your comments and thorough review! :)
>> Responses inline.
>> 
>> On 4 Apr 2017, at 1:57, Brent Royal-Gordon wrote:
>> 
>> 
>> On Apr 3, 2017, at 1:31 PM, Itai Ferber via swift-evolution 
>> <[email protected] <mailto:[email protected]>> wrote:
>> Hi everyone,
>> 
>> With feedback from swift-evolution and additional internal review, we've 
>> pushed updates to this proposal, and to the Swift Archival & Serialization 
>> proposal.
>> Changes to here mostly mirror the ones made to Swift Archival & 
>> Serialization, but you can see a specific diff of what's changed here. Full 
>> content below.
>> 
>> We'll be looking to start the official review process very soon, so we're 
>> interested in any additional feedback.
>> 
>> Thanks!
>> 
>> — Itai
>> 
>> This is a good revision to a good proposal.
>> 
>> I'm glad `CodingKey`s now require `stringValue`s; I think the intended 
>> semantics are now a lot clearer, and key behavior will be much more reliable.
>> 
>> Agreed
>> 
>> 
>> I like the separation between keyed and unkeyed containers (and I think 
>> "unkeyed" is a good name, though not perfect), but I'm not quite happy with 
>> the unkeyed container API. Encoding a value into an unkeyed container 
>> appends it to the container's end; decoding a value from an unkeyed 
>> container removes it from the container's front. These are very important 
>> semantics that the method names in question do not imply at all.
>> 
>> I think that consistency of phrasing is really important here, and the 
>> action words "encode" and "decode" are even more important to connote than 
>> the semantics of pushing and popping.
>> (Note that there need not be specific directionality to an unkeyed container 
>> as long as the ordering of encoded items is eventually maintained on 
>> decode.) But on a practical note, names like encodeAtEnd and decodeFromFront 
>> (or similar) don't feel like they communicate anything much more useful than 
>> the current encode/decode.
>> 
>> 
>> Certain aspects of `UnkeyedDecodingContainer` also feel like they do the 
>> same things as `Sequence` and `IteratorProtocol`, but in different and 
>> incompatible ways. And I certainly think that the `encode(contentsOf:)` 
>> methods on `UnkeyedEncodingContainer` could use equivalents on the 
>> `UnkeyedDecodingContainer`. Still, the design in this area is much improved 
>> compared to the previous iteration.
>> 
>> Which aspects of Sequence and IteratorProtocol do you feel like you're 
>> missing on UnkeyedDecodingContainer? Keep in mind that methods on 
>> UnkeyedDecodingContainer must be able to throw, and an 
>> UnkeyedDecodingContainercan hold heterogeneous items whose type is not 
>> known, two things that Sequence and IteratorProtocol do not do.
>> 
>> In terms of an equivalent to encode(contentsOf:), keep in mind that this 
>> would only work if the collection you're decoding is homogeneous, in which 
>> case, you would likely prefer to decode an Array over getting an unkeyed 
>> container, no? (As soon as conditional conformance arrives in Swift, we will 
>> be able to express extension Array : Decodable where Element : Decodable { 
>> ... } making decoding homogeneous arrays trivial.)
>> 
>> 
>> (Tiny nitpick: I keep finding myself saying "encode into", not "encode to" 
>> as the API name suggests. Would that be a better parameter label?)
>> 
>> On a personal note here — I agree with you, and had originally used "into". 
>> However, we've reviewed our APIs and more often have balanced from:/to: 
>> rather than from:/into: on read/write/streaming calls. We'd like to rein 
>> these in a bit and keep them consistent within our naming guidelines, as 
>> much as possible.
>> 
>> 
>> I like the functionality of the `userInfo` dictionary, but I'm still not 
>> totally satisfied casting out of `Any` all the time. I might just have to 
>> get over that, though.
>> 
>> I think this is the closest we can get to a pragmatic balance between 
>> dynamic needs and static guarantees. :)
>> 
>> 
>> I wonder if `CodingKey` implementations might ever need access to the 
>> `userInfo`. I suppose you can just switch to a different set of `CodingKeys` 
>> if you do.
>> 
>> I don't think CodingKey should ever know about userInfo — CodingKeys should 
>> be inert data. If you need to, use the userInfo to switch to a different set 
>> of keys, as you mention.
>> 
>> 
>> Should there be a way for an `init(from:)` implementation to determine the 
>> type of container in the encoder it's just been handed? Or perhaps the 
>> better question is, do we want to promise users that all decoders can tell 
>> the difference?
>> 
>> I think it would be very rare to need this type of information. If a type 
>> wants to encode as an array or as a dictionary conditionally, the context 
>> for that would likely be present in userInfo.
>> If you really must try to decode regardless, you can always try to grab one 
>> container type from the decoder, and if it fails, attempt to grab the other 
>> container type.
>> 
>> 
>> * * *
>> 
>> I went ahead and implemented a basic version of `Encoder` and `Encodable` in 
>> a Swift 3 playground, just to get a feel for this system in action and 
>> experiment with a few things. A few observations:
>> 
>> Lots to unpack here, let's go one by one. :)
>> 
>> 
>> * I think it may make sense to class-constrain some of these protocols. 
>> `Encodable` and its containers seem to inherently have reference 
>> semantics—otherwise data could never be communicated from all those `encode` 
>> calls out to the ultimate caller of the API. Class-constraining would 
>> clearly communicate this to both the implementer and the compiler. `Decoder` 
>> and its containers don't *inherently* have reference semantics, but I'm not 
>> sure it's a good idea to potentially copy around a lot of state in a value 
>> type.
>> 
>> I don't think class constraints are necessary. You can take a look at the 
>> current implementation of JSONEncoder and JSONDecoder here 
>> <https://github.com/itaiferber/swift/blob/3c59bfa749adad2575975e47130b28b731f763e0/stdlib/public/SDK/Foundation/JSONEncoder.swift>
>>  (note that this is still a rough implementation and will be updated soon). 
>> The model I've followed there is that the encoder itself (_JSONEncoder) has 
>> reference semantics, but the containers (_JSONKeyedEncodingContainer, 
>> _JSONUnkeyedEncodingContainer) are value-type views into the encoder itself.
>> 
>> Keep in mind that during the encoding process, the entities created most 
>> often will be containers. Without some additional optimizations in place, 
>> you end up with a lot of small, short-lived class allocations as containers 
>> are brought into and out of scope.
>> By not requiring the class constraints, it's at least possible to make all 
>> those containers value types with references to the shared encoder.
>> 
>> 
>> * I really think that including overloads for every primitive type in all 
>> three container types is serious overkill. In my implementation, the 
>> primitive types' `Encodable` conformances simply request a 
>> `SingleValueEncodingContainer` and write themselves into it. I can't imagine 
>> any coder doing anything in their overloads that wouldn't be compatible with 
>> that, especially since they can never be sure when someone will end up using 
>> the `Encodable` conformance directly instead of the primitive. So what are 
>> all these overloads buying us? Are they just avoiding a generic dispatch and 
>> the creation of a new `Encoder` and perhaps a 
>> `SingleValueEncodingContainer`? I don't think that's worth the increased API 
>> surface, the larger overload sets, or the danger that an encoder might 
>> accidentally implement one of the duplicative primitive encoding calls 
>> inconsistently with the others.
>> 
>> To be clear: In my previous comments, I suggested that we should radically 
>> reduce the number of primitive types. That is not what I'm saying here. I'm 
>> saying that we should always use a single value container to encode and 
>> decode primitives, and the other container types should always use 
>> `Encodable` or `Decodable`. This doesn't reduce the capabilities of the 
>> system at all; it just means you only have to write the code to handle a 
>> given primitive type one time instead of three.
>> 
>> Having implemented these myself multiple times, I agree — it can be a pain 
>> to repeat these implementations, and if you look at the linked 
>> implementations above, funneling to one method from all of those is exactly 
>> what I do (and in fact, this can be shortened significantly, which I plan on 
>> doing soon).
>> 
>> There is a tradeoff here between ease of use for the end consumer of the 
>> API, and ease of coding for the writer of a new Encoder/Decoder, and my 
>> argument will always be for the benefit of the end consumer. (There will be 
>> orders of magnitude more end consumers of this API than those writing new 
>> Encoders and Decoders 😉)
>> Think of the experience for the consumer of this API, especially someone 
>> learning it for the first time. It can already be somewhat of a hurdle to 
>> figure out what kind of container you need, but even once you get a keyed 
>> container (which is what we want to encourage), then what? You start typing 
>> container.enc... and in the autocomplete list in Xcode, the only thing that 
>> shows up is one autocomplete result: encode(value: Encodable, forKey: ...) 
>> Consider the clarity (or lack thereof) of that, as opposed to seeing 
>> encode(value: Int, forKey: ...), encode(value: String, forKey: ...), etc. 
>> Given a list of types that users are already familiar with helps immensely 
>> with pushing them in the right direction and reducing cognitive load. When 
>> you see String in that list, you don't have to question whether it's 
>> possible to encode a string or not, you just pick it. I have an Int8, can I 
>> encode it? Ah, it's in the list, so I can.
>> 
>> Even for advanced users of the API, though, there's something to be said for 
>> static guarantees in the overloading. As someone familiar with the API (who 
>> might even know all the primitives by heart), I might wonder if the Encoder 
>> I'm using has correctly switched on the generic type. (It would have to be a 
>> dynamic switch.) Did the implementer remember to switch on Int16 correctly? 
>> Or did they forget it and will I be falling into a generic case which is not 
>> appropriate here?
>> 
>> When it comes to overloads vs. dynamically switching on a generic type, I 
>> think we would generally prefer the static type safety. As a consumer of the 
>> API I want to be sure that the implementer of the Encoder I'm using was 
>> aware of these primitive types in some way, and that the compiler helped 
>> them too to make sure they didn't, say, forget to switch on Data.self. As a 
>> writer of Encoders, yes, this is a pain, but a sacrifice I'm willing to make 
>> for the sake of the consumer.
>> 
>> Let's take a step back, though. This is mostly annoying to implement because 
>> of the repetition, right? If someone were to put together a proposal for a 
>> proper macro system in Swift, which is really what we want here, I wouldn't 
>> be sad. 😉
>> 
> There's also an argument of API surface area. As a user or implementer of the 
> API, it's much less intimidating to load the documentation for a protocol and 
> see one central function than many overloads.
> 
> I've used many serialization third-party frameworks in Swift. None of them 
> defined all those overloads, and more importantly, I never saw any user of 
> those APIs post an issue to GitHub where the cause could be traced back to 
> the lack of those overloads.
> 
> These overloads look to me like remnants of Codable's NSCoding influences 
> instead of an API reimagined for Swift.
> 
> For the same reasons, I continue to believe that decode functions should 
> overload on the return type. If we follow the arguments in favor of providing 
> a type argument, then why don't we also have type arguments for encoders: 
> encode(_ value: T?, forKey key: Key, as type: T.self)? I'm not advocating 
> that: I'm just pushing the argument to its logical conclusion to explain why 
> I don't understand it.

I don’t see a way for a call to encode to become ambiguous by omitting the type 
argument, whereas the same is not true for a return value from decode. The two 
seem fundamentally different.

- Tony

>> 
>> * And then there's the big idea: Changing the type of the parameter to 
>> `encode(to:)` and `init(from:)`.
>> 
>> ***
>> 
>> While working with the prototype, I realized that the vast majority of 
>> conformances will immediately make a container and then never use the 
>> `encoder` or `decoder` again. I also noticed that it's illegal to create 
>> more than one container from the same coder, and there are unenforceable 
>> preconditions to that effect. So I'm wondering if it would make sense to not 
>> pass the coder at all, but instead have the conforming type declare what 
>> kind of container it wants:
>> 
>> extension Pet: Codable {
>> init(from container: KeyedDecodingContainer<CodingKeys>) throws {
>> name = try container.decode(String.self, forKey: .name)
>> age = try container.decode(Int.self, forKey: .age)
>> }
>> 
>> func encode(to container: KeyedEncodingContainer<CodingKeys>) throws {
>> try container.encode(name, forKey: .name)
>> try container.encode(age, forKey: .age)
>> }
>> }
>> 
>> extension Array: Encodable where Element: Encodable {
>> init(from container: UnkeyedDecodingContainer) throws {
>> self.init()
>> while !container.isAtEnd {
>> append(try container.decode(Element.self))
>> }
>> }
>> 
>> func encode(to container: UnkeyedEncodingContainer) throws {
>> container.encode(contentsOf: self)
>> }
>> }
>> 
>> I think this could be implemented by doing the following:
>> 
>> 1. Adding an associated type to `Encodable` and `Decodable` for the type 
>> passed to `encode(to:)`/`init(from:)`.
>> 
>> This is already unfortunately a no-go. As mentioned in other emails, you 
>> cannot override an associatetype in a subclass of a class, which means that 
>> you cannot require a different container type than your superclass. This is 
>> especially problematic in the default case where we'd want to encourage 
>> types to use keyed containers — every type should have its own keys, and 
>> you'd need to have a different keyed container than your parent, keyed on 
>> your keys.
>> 
>> Along with that, since the typealias would have to be at least as visible as 
>> your type (potentially public), it would necessitate that your key type 
>> would be at least as public as your type as well. This would expose your 
>> type's coding keys, which is prohibitive. (Consider what this would mean for 
>> frameworks, for instance.)
>> 
>> Finally, this also means that you could not request different container 
>> types based on context — a type could not offer both a dictionary 
>> representation and a more efficient array representation, since it can only 
>> statically request one container type.
>> 
>> 
>> 2. Creating protocols for the types that are permitted there. Call them 
>> `EncodingSink` and `DecodingSource` for now.
>> 
>> 3. Creating *simple* type-erased wrappers for the `Unkeyed*Container` and 
>> `SingleValue*Container` protocols and conforming them to `EncodingSink` and 
>> `DecodingSource`. These wouldn't need the full generic-subclass dance 
>> usually used for type-erased wrappers; they just exist so you can strap 
>> initializers to them. In a future version of Swift which allowed 
>> initializers on existentials, we could probably get rid of them.
>> 
>> (Incidentally, if our APIs always return a type-erased wrapper around the 
>> `Keyed*ContainerProtocol` types, there's no actual need for the underlying 
>> protocols to have a `Key` associated type; they can use `CodingKey` 
>> existentials and depend on the wrapper to enforce the strong key typing. 
>> That would allow us to use a simple type-erased wrapper for 
>> `Keyed*Container`, too.)
>> 
>> 4. For advanced use cases where you really *do* need to access the encoder 
>> in order to decide which container type to use, we would also need to create 
>> a simple type-erased wrapper around `Encoder` and `Decoder` themselves, 
>> conforming them to the `Sink`/`Source` protocols.
>> 
>> This might address my last point above, but then what useful interface would 
>> EncodingSink and DecodingSource have if a type conforming to EncodingSink 
>> could be any one of the containers or even a whole encoder itself?
>> 
>> 
>> 5. The Source/Sink parameter would need to be `inout`, unless we *do* end up 
>> class-constraining things. (My prototype didn't.)
>> 
>> There are lots of little details that change too, but these are the broad 
>> strokes.
>> 
>> Although this technically introduces more types, I think it actually 
>> simplifies the design for people who are just using the `Codable` protocol. 
>> All they have to know about is the `Codable` protocol, the magic 
>> `CodingKeys` type, the three container types (realistically, probably just 
>> the `KeyedEncoding/DecodingContainer`), and the top-level encoders they want 
>> to use. Most users should never need to know about the members of the 
>> `Encoder` protocol; few even need to know about the other two container 
>> types. They don't need to do the "create a container" dance. The thing would 
>> just work with a minimum of fuss.
>> 
>> Meanwhile, folks who write encoders *do* deal with a bit more complexity, 
>> but only because they have to be aware of more type-erased wrappers. In 
>> other respects, it's simpler for them, too. Keyed containers don't need to 
>> be generic, and they have a layer of Foundation-provided wrappers above them 
>> that can help enforce good behavior and (probably) hide the implementation a 
>> little bit more. I think that overall, it's probably better for them, too.
>> 
>> Thoughts?
>> 
>> For what it's worth, the way to introduce these three different types of 
>> encoding without the use of associated types is to split the Codable 
>> protocol up into three protocols, which we've tried in the past 
>> <https://github.com/itaiferber/swift-evolution/blob/swift-archival-serialization/proposals/XXXX-swift-archival-serialization.md#alternatives-considered>
>>  (bullet #4). Unfortunately, the results are not great — an even bigger 
>> explosion of types, overloads, etc.
>> 
>> While I agree that the current approach of dynamically requesting containers 
>> is, well, dynamic, the benefit of not exposing your keys publicly and 
>> allowing encoding of classes is a big win in comparison, I think.
>> 
>> I am curious, though, about your comment above on preconditions being 
>> unenforceable, because this is certainly something we would want to hammer 
>> out before releasing. What cases are you thinking of that are unenforceable?
>> 
>> 
>> -- 
>> Brent Royal-Gordon
>> Architechies
>> 
>> Again, thanks for your thorough review! Looking forward to further comments. 
>> :)
>> 
>> _______________________________________________
>> 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>
> _______________________________________________
> 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>
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to