> 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.
>>
>> 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.
>> }
>> // 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.
> 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.
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.)
> 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.)
> 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.
--
Brent Royal-Gordon
Architechies
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution