> On Apr 5, 2017, at 1:44 PM, David Hart via swift-evolution 
> <swift-evolution@swift.org> wrote:
> 
>>> 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.
> 
> When decoding to a property, there will be no ambiguity. And for other cases, 
> Swift developers are already quite used to handling that kind of ambiguity, 
> like for literals:
> 
> let x: UInt = 10
> let y = 20 as CGFloat

But in the literal case, they *don't* have to deal with ambiguity for two 
reasons:

1. The literal provides some hint of the type; integer, float, string, array, 
and dictionary literals are all easy to distinguish from one another.

2. Each literal syntax has a default type. That is not true and cannot *be* 
true for `decode()`.

Plus there's a third reason:

3. `Decoder` doesn't guarantee there's a safety net if you use the wrong type. 
If you, say, decode an `Int32` using `Int64`, a decoder for some low-level 
binary type would be perfectly within its rights to read part of the next 
field, access everything subsequent to that point in a misaligned way, and go 
totally off the rails (as long as it doesn't violate memory safety).

That third reason is exactly the same as why `unsafeBitCast(_:to:)`, 
`bindMemory(to:capacity:)`, etc. all have a type-pinning parameter. Although 
Swift places no restrictions on return-type inference, in practice the core 
team thinks unconstrained return types are dangerous and should be used with 
care, only permitted when an API explicitly exists to ease conversions between 
different types. (`numericCast(_:)` is one example; I'm not sure if there are 
any others.) That's just their opinion, and of course you are always free to 
disagree with them, but I think it's a solid and easily justified one.

Besides, if you really want this, it's easy to add with a pair of extensions:

        extension KeyedDecodingContainer {
                func decode<T: Encodable>(forKey key: Key) throws -> T {
                        return try decode(T.self, forKey: key)
                }
        }
        extension UnkeyedDecodingContainer {
                func decode<T: Encodable>() throws -> T {
                        return try decode(T.self)
                }
        }

(P.S. There might be a way to square this circle: If a CodingKey knew its type, 
the mere act of providing a CodingKey would be enough to pin the type. This 
would not only avoid both an explicit type-pinning parameter *and* 
unconstrained generic return types, it would also prevent you from accidentally 
specifying the wrong type during decoding. Rough example:

        protocol CodingKey {
                associatedtype Value: Encodable
                
                var stringValue: String { get }
                var intValue: Int? { get }
                
                init(stringValue: String, intValue: Int?, as _: Value.Type)
        }
        extension CodingKey {
                var intValue: Int { return nil }
        }
        
        extension Person: Decodable {
                struct CodingKeys<Value>: CodingKey {
                        let stringValue: String
                        
                        init(stringValue: String, intValue: Int? = nil, as _: 
Value.Type) {
                                self.stringValue = stringValue
                        }
                        
                        static let name = CodingKeys(stringValue: "name", as: 
String.self)
                        static let age = CodingKeys(stringValue: "age", as: 
Int.self)
                        static let pets = CodingKeys(stringValue: "pets", as: 
[Pet].self)
                }
                
                init(from decoder: Decoder) throws {
                        let c = try decoder.container(keyedBy: CodingKeys.self)
                        
                        name = c.decode(.name)
                        age = c.decode(.age)
                        pets = c.decode(.pets)
                }
        }

But this doesn't work for two reasons: You can't pass an entire generic type to 
`container(keyedBy:)` and you can't put constants in a generic type. You also 
lose the ability to construct a CodingKey from a String or Int, you lose the 
guarantee that all possible instances are known at compile time (I could 
imagine a linter checking that you've encoded/decoded all CodingKeys), and you 
lose all the convenient magic of enum raw types. You could address the "doesn't 
work" issues, but only by adding even more boilerplate. Still, if there's a way 
to do this that *doesn't* have so many disadvantages, we should seriously 
consider taking it.)

-- 
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to