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>