[swift-evolution] [Request for Feedback] Providing defaults for <Codable> reading and writing.

Itai Ferber iferber at apple.com
Tue Jul 11 12:16:49 CDT 2017


Hi Wil,

Thanks for putting this together! My biggest thought on this is — what 
does this provide that you can’t already do yourself today?
Since you have to go through the work to put together default values and 
override `init(from:)` and `encode(to:)` to use them, I’m wondering 
whether this saves you any work over doing something like the following:

```swift
struct Theme {
     private static let _defaultName = ""
     private static let _defaultStyles: [String] = []

     public let name: String
     public let styles: [String]

     private enum CodingKeys : String, CodingKey {
         case name
         case styles
     }

     public init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         name = try? decoder.decode(String.self, forKey: .name) ?? 
Theme._defaultName
         styles = try? decoder.decode([String.self], forKey: .styles) ?? 
Theme._defaultStyles
     }

     public func encode(to encoder: Encoder) throws {
         var container = encoder.container(keyedBy: CodingKeys.self)
         if (name != Theme._defaultName) try container.encode(name, 
forKey: .name)
         if (styles != Theme._defaultStyles) try 
container.encode(styles, forKey: .styles)
     }
}
```

This reads just as clearly to me as the `defaults:` variation while 
having the added benefit of low complexity and stronger type safety (as 
there’s no `as!`-casting down from `Any`, which could fail).

Thoughts?

— Itai

On 10 Jul 2017, at 17:16, William Shipley via swift-evolution wrote:

> Automatic substitution / removal of default values is very useful when 
> reading or writing a file, respectively, and should be supported by 
> the <Codable> family of protocols and objects:
>
> • When reading, swapping in a default value for missing or corrupted 
> values makes it so hand-created or third-party-created files don’t 
> have to write every single value to make a valid file, and allows 
> slightly corrupted files to auto-repair (or get close, and let the 
> user fix up any data that needs it after) rather than completely fail 
> to load. (Repairing on read creates a virtuous cycle with user-created 
> files, as the user will get _some_ feedback on her input even if 
> she’s messed up, for example, the type of one of the properties.)
>
> • When writing, providing a default value allows the container to 
> skip keys that don’t contain useful information. This can 
> dramatically reduce file sizes, but I think its other advantages are 
> bigger wins: just like having less source code makes a program easier 
> to debug, having less “data code” makes files easier to work with 
> in every way — they’re easier to see differences in, easier to 
> determine corruption in, easier to edit by hand, and easier to learn 
> from.
>
>
> My first pass attempt at adding defaults to Codable looks like this:
>
>
> public class ReferencePieceFromModel : Codable {
>
>     // MARK: properties
>     public let name: String = ""
>     public let styles: [String] = []
>
>
>     // MARK: <Codable>
>     public required init(from decoder: Decoder) throws {
>         let container = try decoder.container(keyedBy: 
> CodingKeys.self)
>
>         self.name = container.decode(String.self, forKey: .name, 
> defaults: type(of: self).defaultsByCodingKey)
>         self.styles = container.decode([String].self, forKey: .styles, 
> defaults: type(of: self).defaultsByCodingKey)
>     }
>     public func encode(to encoder: Encoder) throws {
>         var container = encoder.container(keyedBy: CodingKeys.self)
>
>         try container.encode(name, forKey: .name, defaults: type(of: 
> self).defaultsByCodingKey)
>         try container.encode(styles, forKey: .styles, defaults: 
> type(of: self).defaultsByCodingKey)
>     }
>     private static let defaultsByCodingKey: [CodingKeys : Any] = [
>         .name : "",
>         .styles : [String]()
>     ]
>
>
>     // MARK: private
>     private enum CodingKeys : String, CodingKey {
>         case name
>         case styles
>     }
> }
>
> With just a couple additions to the Swift libraries:
>
> extension KeyedDecodingContainer where Key : Hashable {
>     func decode<T>(_ type: T.Type, forKey key: Key, defaults: [Key : 
> Any]) -> T where T : Decodable {
>         if let typedValueOptional = try? decodeIfPresent(T.self, 
> forKey: key), let typedValue = typedValueOptional {
>             return typedValue
>         } else {
>             return defaults[key] as! T
>         }
>     }
> }
>
> extension KeyedEncodingContainer where Key : Hashable {
>     mutating func encode<T>(_ value: T, forKey key: Key, defaults: 
> [Key : Any]) throws where T : Encodable & Equatable {
>         if value != (defaults[key] as! T) {
>             try encode(value, forKey: key)
>         }
>     }
>
>     mutating func encode<T>(_ value: [T], forKey key: Key, defaults: 
> [Key : Any]) throws where T : Encodable & Equatable { // I AM SO SORRY 
> THIS IS ALL I COULD FIGURE OUT TO MAKE [String] WORK!
>         if value != (defaults[key] as! [T]) {
>             try encode(value, forKey: key)
>         }
>     }
> }
>
>
> (Note the horrible hack on KeyedEncodingContainer where I had to 
> special-case arrays of <Equatable>s, I guess because the compiler 
> doesn’t know an array of <Equatable>s is Equatable itself?)
>
>
> Problems with this technique I’ve identified are:
>
> ⑴ It doesn’t allow one to add defaults without manually writing 
> the init(from:) and encode(to:), ugh.
> ⑵ The programmer has to add 'type(of: self).defaultsByCodingKey’ 
> to every call, ugh.
>
> Both of these could possibly be worked around if we could add an 
> optional method to the <Codable> protocol, that would look something 
> like:
>
>     public static func default<Key>(keyedBy type: Key.Type, key: Key) 
> -> Any? where Key : CodingKey
>
> (the above line isn’t tested and doubtlessly won’t work as typed 
> and has tons of think-os.)
>
> This would get called by KeyedEncodingContainers and 
> KeyedDecodingContainers only for keys that are Hashable (which I think 
> is all keys, but you can stick un-keyed sub-things in Keyed containers 
> and obviously those can’t have defaults just for them) and the 
> container would be asked to do the comparison itself, with ‘==‘.
>
> Something I haven’t tried to address here is what to do if values 
> are NOT <Equatable> — then of course ‘==‘ won’t work. One 
> approach to this would be to provide a way for the static func above 
> to return ‘Hey, I don’t have anything meaningful for you for this 
> particular property, because it’s not Equatable.’ This could be as 
> simple as returning ‘nil’, which would also be a decent way to 
> say, “This property has no meaningful default” which is also 
> needed.
>
> Alternatively, one could imagine adding TWO callbacks in the <Codable> 
> for this kind of case, which are essentially *WAVES HANDS*:
>
>      public static func isThisValueTheDefault(_ value: Any, forKey 
> key: Self.Key) throws -> Any?
>      public static func defaultValue<Key>(keyedBy type: Key.Type, key: 
> Key) -> Any? where Key : CodingKey
>
> These might also need a 'keyedBy type: Key.Type’ parameter — to be 
> honest I haven’t messed with different key spaces so I’m not sure 
> how they work. Also I’m not the best at generics yet. (At this point 
> I’m not even sure if protocols can contain ‘class’ functions, so 
> maybe none of this would work.)
>
> Another advantage to the two-method approach (besides not requiring 
> the values to be < Equatable >) is that it allows one to provide 
> defaults for floating values, which can often be changed just by 
> floating-point error by like 0.00000000001 and then end up registering 
> false changes. In the isValueDefault(…) the programmer could 
> implement a comparison with a ‘slop’ so if the encoder were about 
> to write 0.000000000001 and the default were 0 nothing would be 
> written.
>
>
> -Wil
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170711/39527c0e/attachment.html>


More information about the swift-evolution mailing list