Hi there!
Double and Float both conform to Codable. Why does Float80 not conform to Codable? Is there any specific reason?
Best regards!
strike
Hi there!
Double and Float both conform to Codable. Why does Float80 not conform to Codable? Is there any specific reason?
Best regards!
strike
There's nothing really keeping Float80
from conforming to Codable
short of us needing to decide what the encoded representation might look like. Because it isn't a Codable
primitive like Float
and Double
, it would need a concrete implementation of init(from:)
and encode(to:)
.
There are a few options there but we'd need to decide on the best way to do it. Any thoughts?
Ok.
Float80
conforms to LosslessStringConvertible
. Well. My private extension to make Float80
conforming to Codable
looks just like a crowbar and fits for my needs ("There is never ever a optional float outta here."):
Please do not laugh!
extension Float80: Codable {
private enum CodingKeys: String, CodingKey {
case string = "stringValue"
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let stringRep: String = "\(self)" /* Float80 conforms to LosslessStringConvertible */
try container.encode(stringRep, forKey: .string)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let string = try container.decode(String.self, forKey: .string)
if let f = Float80.init(string) {
self = f
}
else {
self = Float80.nan
}
}
}
That's certainly a legitimate way to encode a Float80
, and may be slightly less ambiguous than encoding as a string directly (e.g. encoder.singleValueContainer().encode("\(self)")
); it also gives us the flexibility to change representations in the future in a container-type-compatible way.
Another option would be to somehow separate the value into two Double
components and encode as a two-value array (since we don't have tuples), which may save a bit on space. /cc @scanon Thoughts/preferences on how we might represent this efficiently?
You can't easily split a Float80
into two Double
s. If you wanted to do something like that, splitting the encoding into a UInt16
and a UInt64
would be a better option, and somewhat more efficient than encoding as a string. The downside would be that you'd give up the easy human-readability, and it would generally be not very useful for anything else. So string seems like a good solution.
Hi!
Would it be worth to make a proposal?
Best regards!
strike
This might even be something that could be taken as a bug-fix (no proposal needed). @itaiferber?
I think we would need a proposal here as this is an API change (and a breaking one at that for anyone who’s written their own Float80
conformance to Codable
). Should be a simple pitch though!
Is there some way that Codable could handle everything that is LosslessStringConvertible?
Sure! We could have extension LosslessStringConvertible where Self: Codable { ... }
to give a default implementation to all LosslessStringConvertible
types. The question would be whether a generalization like this would be more appropriate than requiring those types to define their conformances.
Another consideration would be whether this extension would conflict with other similar extensions — for instance, we have extensions on RawRepresentable
types whose RawValue
types are primitive Codable
types. If you have a type which matches that criteria and is also LosslessStringConvertible
, which implementation wins out? (It’d be ambiguous and you’d have to define your own, IIRC, losing out on the benefit of both default implementations.)
Right, and more generally, I think it's fair to say that even if it could be done without ambiguity, it's still a question whether it should be done.
In general, I would argue that the two are independent features. It's likely the minority case that both converge such that the string description is also the exact way one prefers to encode a type independent of output format. It's probably quite appropriate for standard library primitive types not otherwise supported by Codable. But for third-party types, I think a user should actively choose this implementation if it makes sense and override any synthesized conformance, rather than having a default conformance make that assumption and trump the synthesized one.
Agreed! To clarify, more strongly, I think Codable
and LosslessStringConvertible
are orthogonal features which may overlap, but I don't think should be made to. (This topic has come up in the past in a slightly different context but in general I think it would be more appropriate for developers to provide appropriate intent by disambiguating on their own.)
I think the following is a viable implementation:
public extension LosslessStringConvertible where Self: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode("\(self)")
}
}
public extension LosslessStringConvertible where Self: Decodable {
init(from decoder: Decoder) throws {
let stringRep = try decoder.singleValueContainer().decode(String.self)
self.init(stringRep)!
}
}
// Opt in.
extension Float80: Codable {}
// Test.
import Foundation
let f80 = Float80(3.142)
let encodedData = try JSONEncoder().encode(f80)
print(String(data: encodedData, encoding: .utf8)!)
let decodedF80 = try JSONDecoder().decode(Float80.self, from: encodedData)
print(decodedF80 == f80)
The difference is that you have to opt in. Therefore concerns about all LosslessStringConvertible
s suddenly becoming Codable
are mitigated and you can still do your own implementation if you wish.
Unfortunately I think there is a Codable
bug, see Problem with Encoder.singleValueContainer, and so the above currently doesn't work :(.
The issue with doing this unfortunately is not that it opts LosslessStringConvertible
types into Codable
automatically (since that can’t be done unless we make LosslessStringConvertible
inherit from Codable
) but that you cannot be Codable
and LosslessStringConvertible
and ask the compiler to synthesize an implementation of Codable
(since the implementation is already given by the extension). This tightly couples the intent of adopting LosslessStringConvertible
with the intent of adopting Codable
, which is not necessarily correct (nor what developers want).
(As far as the JSONEncoder
issue specifically goes — see my reply in the linked thread.)
First a version that doesn't use a single container and therefore works with Swift 4:
fileprivate enum StringCoding: String, CodingKey {
case key = "stringLiteral"
}
public extension LosslessStringConvertible where Self: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCoding.self)
try container.encode("\(self)", forKey: .key)
}
}
public extension LosslessStringConvertible where Self: Decodable {
init(from decoder: Decoder) throws {
let stringRep = try decoder.container(keyedBy: StringCoding.self).decode(String.self, forKey: .key)
self.init(stringRep)!
}
}
// Opt in.
extension Float80: Codable {}
// Test.
import Foundation
let f80 = Float80(3.142)
let encodedF80 = try JSONEncoder().encode(f80)
print(String(data: encodedF80, encoding: .utf8)!) // {"stringLiteral":"3.141999999999999904"}
let decodedF80 = try JSONDecoder().decode(Float80.self, from: encodedF80)
print(decodedF80 == f80) // T
You have to opt in, e.g. the following is LosslessStringConvertible but not Codable because it is not declared as Codable:
// Check that you can still write a Lossless... that isn't Codable.
struct StringOnly: LosslessStringConvertible { // Not Codable.
let f: Float80
init?(_ description: String) {
let optional = Float80(description)
guard let value = optional else {
return nil
}
f = value
}
var description: String {
return f.description
}
}
let sO = StringOnly("3.141999999999999904")!
print(sO is Codable) // F
Also you can still write your own implementation if you need to:
struct StringAndCodable: LosslessStringConvertible, Codable, Equatable {
let f: Float80
init?(_ description: String) {
let optional = Float80(description)
guard let value = optional else {
return nil
}
f = value
}
var description: String {
return f.description
}
enum Key: String, CodingKey {
case key = "key"
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Key.self)
try container.encode("\(self)", forKey: .key)
}
init(from decoder: Decoder) throws {
let stringRep = try decoder.container(keyedBy: Key.self).decode(String.self, forKey: .key)
self.init(stringRep)!
}
}
let sAC = StringAndCodable("3.141999999999999904")
let encodedSAC = try JSONEncoder().encode(sAC)
print(String(data: encodedSAC, encoding: .utf8)!) // {"key":"3.141999999999999904"} - "key" not "stringLiteral"
let decodedSAC = try JSONDecoder().decode(StringAndCodable.self, from: encodedSAC)
print(sAC == decodedSAC) // T
Therefore I think the extension as proposed are viable. The tricky bit of the code is that extensions to Lossless... constrain Self to be Codable and are therefore opt-in.
Indeed — you can always override an implementation given in an extension this way, which is good! The non-starter here is a situation like the following:
import Foundation
struct S : Codable {
let foo: Int
let bar: String
}
let data = try JSONEncoder().encode(S(foo: 42, bar: "hello"))
try data.write(to: URL(fileURLWithPath: "/tmp/somefile"))
// elsewhere
let stored = try Data(contentsOf: URL(fileURLWithPath: "/tmp/somefile"))
let s = try JSONDecoder().decode(S.self, from: stored)
print(s) // => S(foo: 42, bar: "hello")
In version 2 of my app, LosslessStringConvertible
conformance is added to S
for completely unrelated purposes:
// Given these extensions:
extension LosslessStringConvertible where Self : Encodable { ... }
extension LosslessStringConvertible where Self : Decodable { ... }
extension S : LosslessStringConvertible { ... }
// Oops: Error raised at top level: Swift.EncodingError.invalidValue(<s foo:42 bar:"hello">, Swift.EncodingError.Context(codingPath: [], debugDescription: "Top-level S encoded as string JSON fragment.", underlyingError: nil))
let data = try JSONEncoder().encode(S(foo: 42, bar: "hello"))
try data.write(to: URL(fileURLWithPath: "/tmp/somefile"))
// Elsewhere:
let stored = try Data(contentsOf: URL(fileURLWithPath: "/tmp/somefile"))
let s = try JSONDecoder().decode(S.self, from: stored) // Oops: Error raised at top level: Swift.DecodingError.typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode String but found a dictionary instead.", underlyingError: nil))
print(s)
Where S
was previously depending on the compiler-synthesized implementation of Codable
, it now gets the implementation as defined by the LosslessStringConvertible
extensions, with no way to request the old serialization format without overriding and implementing it manually. Worse is that this is an implicit change — there is no indication that anything is different until I actually try to encode and decode, at which point it might be too late: files written to disk may be in a new, undesired format, and old files are no longer readable.
Perhaps this behavior is a good indicator for needing a strong, explicit signal to the compiler that synthesized behavior is desired, rather than the more implicit request-via-adoption.
Hi There!
Thank you very much for your thoughts. I see that it''s rather complicated.
Best regards!
strike
Yes you are right. Unlikely as the scenario is, it is possible and therefore needs to be guarded against. I see this as a bug/limitation of Swift though, because:
protocol A {}
extension A {
func f() -> String {
return "A"
}
}
protocol B {}
extension B {
func f() -> String {
return "B"
}
}
struct S: A, B {}
let s = S()
s.f() // Ambiguous
Will report the ambiguity and you can resolve it (I view this as much the same - two competing implementations are inherited):
struct S: A, B {
func f() -> String {
return (self as A).f()
}
}
For Codable the compiler should report the ambiguity and allow a resolution, e.g:
struct S: LosslessStringConvertible, Codable {
func encode(to encoder: Encoder) throws {
(self as LosslessStringConvertible).encode(to: encoder)
}
}
Or
struct S: LosslessStringConvertible, Codable {
func encode(to encoder: Encoder) throws {
(self as #GeneratedEncodable).encode(to: encoder)
}
}
Or a shorthand for the latter could be:
struct S: LosslessStringConvertible, #GeneratedCodable {}
Where #GeneratedCodable
is a pseudo compiler generated protocol with the default version of codable.
Your first example shows protocol extension methods that are statically dispatched, hence the ambiguity, whereas encode(to:) is a protocol requirement and dynamically dispatched--even if you could spell it, both spellings would necessarily dispatch to the same method (if it would work at all--"as LosslessStringConvertible" would not as written because there is no encode(to:) method for that type). This goes back to the point that these are distinct protocols for distinct purposes.
Yes you are right. You need something like:
struct S: LosslessStringConvertible, Codable {
func encode(to encoder: Encoder) throws {
super.LosslessStringConvertible.encode(to: encoder)
}
}
or
struct S: LosslessStringConvertible, Codable {
func encode(to encoder: Encoder) throws {
super.#GeneratedEncodable.encode(to: encoder)
}
}
This ability to specify which super
has come up already in a number of threads as a useful feature.