[swift-evolution] [Request for Feedback] Providing defaults for <Codable> reading and writing.
Itai Ferber
iferber at apple.com
Wed Jul 12 18:33:23 CDT 2017
That’s fair. :)
I think in the time frame of Swift 4, this would be too big of an
addition and would require more thought, but:
1. When the conditional conformance feature arrives in a future Swift
release, a lot of the hacks surrounding `Equatable` can go away here,
because we’ll get things like `Array<Element> : Equatable where
Element : Equatable` and `Array<Element> : Codable where Element :
Codable`
2. This seems like an easily additive feature — overloads taking
defaults can be added after the fact (given a default implementation
which does something similar to what you and Randy suggested):
```swift
// Just an example:
extension KeyedEncodingContainerProtocol {
func encode<T : Codable>(_ value: T, forKey key: Key, defaultValues
defaults: [Key : Any]) throws where T : Equatable {
guard let defaultValue = defaults[key],
value != defaultValue else {
return try encode(value, forKey: key)
}
}
}
extension KeyedDecodingContainerProtocol {
func decode<T : Decodable>(_ type: T.Type, forKey key: Key,
defaultValues defaults: [Key : Any]) throws -> T {
guard let defaultValue = defaults[key] else {
return try decode(type, forKey: key)
}
if let value = try decodeIfPresent(type, forKey: key) {
return value
} else {
return defaultValue
}
}
}
```
On 11 Jul 2017, at 13:16, William Shipley wrote:
> 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 <iferber at apple.com> 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
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170712/ce7a1735/attachment.html>
More information about the swift-evolution
mailing list