Sent from my iPad
> On Mar 19, 2017, at 10:19 PM, Matthew Johnson via swift-evolution > <[email protected]> wrote: > > > > Sent from my iPad > >> On Mar 19, 2017, at 9:14 PM, Brent Royal-Gordon <[email protected]> >> wrote: >> >>> On Mar 19, 2017, at 5:51 PM, Matthew Johnson <[email protected]> wrote: >>> >>> I generally agree with you about casting. However, my dislike isn’t the >>> cast itself, but instead it is the lack of a static guarantee. I’m not >>> sure we’ll find a solution that provides a static guarantee that a required >>> context exists that is also acceptable to the Foundation team. >> >> I don't think we can get a static guarantee that the context is present, but >> I still would like a static guarantee that the context is of the expected >> type. That's what I'm trying to provide here. > > This doesn't do any better job of that than a cast in user code. I can see > two meaningful differences. First, your solution does not allow a user to > see a context if they can't name the type (you can't get it as Any and use > reflection, etc). I don't see this restriction as being beneficial. Second, > your solution introduces several subtle problems mentioned in my last email > which you didn't respond to (overlapping context types, etc). > >> >>>> >>>> protocol Encoder { >>>> // Retrieve the context instance of the indicated type. >>>> func context<Context>(ofType type: Context.Type) -> Context? >>>> >>>> // This context is visible for `encode(_:)` calls from this >>>> encoder's containers all the way down, recursively. >>>> func addContext<Context>(_ context: Context, ofType type: >>>> Context.Type) >>> >>> What happens if you call `addContext` more than once with values of the >>> same type? >> >> It overrides the previous context, but only for the containers created by >> this `encode(to:)` method and any containers nested within them. >> >> (Although that could cause trouble for an encoder which only encodes objects >> with multiple instances once. Hmm.) >> >>> And why do you require the type to be passed explicitly when it is already >>> implied by the type of the value? >> >> As you surmised later, I was thinking in terms of `type` being used as a >> dictionary key; in that case, if you stored a `Foo` into the context, you >> would not later be able to look it up using one of `Foo`'s supertypes. But >> if we really do expect multiple contexts to be rare, perhaps we don't need a >> dictionary at all—we can just keep an array, loop over it with `as?`, and >> return the first (or last?) match. If that's what we do, then we probably >> don't need to pass the type explicitly. > > The array approach is better because at least there is an order to the > contexts and we can assign precise semantics in the presence of overlapping > context types by saying type get the first (most recent) context that can be > cast to the type you ask for. > > That said, I think what you're really trying to model here is a context > stack, isn't it? Why don't we just do that? > >> >>>> } >>>> // Likewise on Decoder >>>> >>>> // Encoder and decoder classes should accept contexts in their >>>> top-level API: >>>> open class JSONEncoder { >>>> open func encode<Value : Codable>(_ value: Value, withContexts >>>> contexts: [Any] = []) throws -> Data >>>> } >>> >>> What happens if more than one context of the same type is provided here? >> >> Fail a precondition, probably. > > I would never support this design. Good news though: the context stack > approach avoids the problem. We allow multiple contexts of the same type to > be on the stack and the topmost context that can be cast to the requested > type is used. > >> >>> Also, it’s worth pointing out that whatever reason you had for explicitly >>> passing the type above you’re not requiring type information to be provided >>> here. Whatever design we have it should be self-consistent. >> >> Yeah. I did this here because there was no way to specify a dictionary >> literal of `(T.Type, T)`, where `T` could be different for different >> elements. >> >>> Do you think it’s really important to allow users to dynamically provide >>> context for children? Do you have real world use cases where this is >>> needed? I’m sure there could be case where this might be useful. But I >>> also think there is some benefit in knowing that the context used for an >>> entire encoding / decoding is the one you provide at the top level. I >>> suspect the benefit of a static guarantee that your context is used for the >>> entire encoding / decoding has a lot more value than the ability to >>> dynamically change the context for a subtree. >> >> The problem with providing all the contexts at the top level is that then >> the top level has to *know* what all the contexts needed are. Again, if >> you're encoding a type from FooKit, and it uses a type from GeoKit, then >> you—the user of FooKit—need to know that FooKit uses GeoKit and how to make >> contexts for both of them. There's no way to encapsulate GeoKit's role in >> encoding. > > The use cases I know of for contexts are really around helping a type choose > an encoding strategy. I can't imagine a real world use case where a Codable > type would have a required context - it's easy enough to choose one strategy > as the default. That said, I can imagine really evil and degenerate API > designs that would require the same type to be encoded differently in > different parts of the tree. I could imagine dynamic contexts being helpful > in solving some of these cases, but often you would need to look at the > codingKeyContext to get it right. > > If you have a concrete real world use case involving module boundaries please > elaborate. I'm having trouble imagining the details about a precise problem > you would solve using dynamic contexts. I get the impression you have > something more concrete in mind than I can think of. > >> >> On the other hand, there *could* be a way to encapsulate it. Suppose we had >> a context protocol: >> >> protocol CodingContext { >> var underlyingContexts: [CodingContext] { get } >> } >> extension CodingContext { >> var underlyingContexts: [CodingContext] { return [] } >> } >> >> Then you could have this as your API surface: >> >> protocol Encoder { >> // Retrieve the context instance of the indicated type. >> func context<Context: CodingContext>(ofType type: Context.Type) >> -> Context? >> } >> // Likewise on Decoder >> >> // Encoder and decoder classes should accept contexts in their >> top-level API: >> open class JSONEncoder { >> open func encode<Value : Codable>(_ value: Value, with context: >> CodingContext? = nil) throws -> Data >> } >> >> And libraries would be able to add additional contexts for dependencies as >> needed. >> >> (Hmm. Could we maybe do this? >> >> protocol Codable { >> associatedtype CodingContextType: CodingContext = Never >> >> func encode(to encoder: Encoder) throws >> init(from decoder: Decoder) throws >> } >> >> protocol Encoder { >> // Retrieve the context instance of the indicated type. >> func context<CodableType: Codable>(for instance: Codable) -> >> CodableType.CodingContextType? >> } >> // Likewise on Decoder >> >> // Encoder and decoder classes should accept contexts in their >> top-level API: >> open class JSONEncoder { >> open func encode<Value : Codable>(_ value: Value, with context: >> Value.CodingContextType? = nil) throws -> Data >> } >> >> That would make sure that, if you did use a context, it would be the right >> one for the root type. And I don't believe it would have any impact on types >> which didn't use contexts.) > > I think this is far more than we need. I think we could just say encoders > and decoders keep a stack of contexts. Calls to encode or decode (including > top level) can provide a context (or an array of contexts which are > interpreted as a stack bottom on left, top on right). When the call returns > the stack is popped to the point it was at before the call. We could also > include an explicit > `func push(contexts: Context...)` This should have been `func push(contexts: Any...)` > method on encoder and decoder to allow a Codable to set context used by all > of its members. All calls to `push` would be popped when the current call to > encode / decode returns. > > Users ask for a context from an encoder / decoder using `func > context<Context>(of: Context.Type) -> Context?`. The stack is searched from > the top to the bottom for a value that can be successfully cast to Context. > >> >>> What benefit do you see in using types as context “keys” rather than >>> something like `CodingUserInfoKey`? As far as I can tell, it avoids the >>> need for an explicit key which you could argue are somewhat redundant (it >>> would be weird to have two context values of the same type in the cases I >>> know of) and puts the cast in the Encoder / Decoder rather than user code. >>> These seem like modest, but reasonable wins. >> >> I also see it as an incentive for users to build a single context type >> rather than sprinkling in a whole bunch of separate keys. I really would >> prefer not to see people filling a `userInfo` dictionary with random >> primitive-typed values like `["json": true, "apiVersion": "1.4"]`; it seems >> too easy for names to clash or people to forget the type they're actually >> using. `context(…)` being a function instead of a subscript is similarly >> about ergonomics: it discourages you from trying to mutate your context >> during the encoding process (although it doesn't prevent it for reference >> types.) >> > > I agree with this sentiment and indicated to Tony the desire to steer people > away from treating this as a dictionary to put a lot of stuff in and towards > defining an explicit context type. This and the fact that keys will feel > pretty arbitrary are behind my desire to avoid the keys and dictionary > approach. > >>> Unfortunately, I don't think there is a good answer to the question about >>> multiple context values with the same type though. I can’t think of a good >>> way to prevent this statically. Worse, the values might not have the same >>> type, but be equally good matches for a type a user requests (i.e. both >>> conform to the same protocol). I’m not sure how a user-defined encoder / >>> decoder could be expected to find the “best” match using semantics that >>> would make sense to Swift users (i.e. following the rules that are kind of >>> the inverse to overload resolution). >>> >>> Even if this were possible there are ambiguous cases where there would be >>> equally good matches. Which value would a user get when requesting a >>> context in that case? We definitely don’t want accessing the context to be >>> a trapping or throwing operation. That leaves returning nil or picking a >>> value at random. Both are bad choices IMO. >> >> If we use the `underlyingContexts` idea, we could say that the context list >> is populated breadth-first and the first context of a particular type >> encountered wins. That would tend to prefer the context "closest" to the >> top-level one provided by the caller, which will probably have the best >> fidelity to the caller's preferences. > > I'm not totally sure I follow you here, but I think you're describing > stack-like semantics that are at least similar to what I have described. I > think the stack approach is a pretty cool one that targets the kinds of > problems multiple contexts are trying to solve more directly than the > dictionary approach would. > >> >> -- >> 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
