You’re right, my current implementation doesn’t win anything over what you’re written - in fact your technique is basically what I wrote at first.
I was trying to work towards encapsulating the behavior in the encoder/decoder so that the automatic init/encode methods could work, so I wanted to introduce my first (more manual) attempt and then say, here’s where I’d like to get with this. -Wil > On Jul 11, 2017, at 10:16 AM, Itai Ferber <[email protected]> wrote: > > 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: > > 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 > [email protected] > https://lists.swift.org/mailman/listinfo/swift-evolution > <https://lists.swift.org/mailman/listinfo/swift-evolution>
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
