Brent, can you share your playground, or perhaps put it up in a repo? I think it would be very useful to help the rest of us evaluate the proposal.
Jon > On Apr 4, 2017, at 4:57 AM, Brent Royal-Gordon via swift-evolution > <[email protected]> wrote: > >> On Apr 3, 2017, at 1:31 PM, Itai Ferber via swift-evolution >> <[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. > > 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. 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. > > (Tiny nitpick: I keep finding myself saying "encode into", not "encode to" as > the API name suggests. Would that be a better parameter label?) > > 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 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. > > 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 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: > > * 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 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. > > * 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:)`. > > 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. > > 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? > > -- > Brent Royal-Gordon > Architechies > > _______________________________________________ > swift-evolution mailing list > [email protected] > https://lists.swift.org/mailman/listinfo/swift-evolution _______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
