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

Reply via email to