[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