Codable Tuples

Swift doesn't let you conform to codable when you have a variable thats a tuple and from what I looked up JSON on its own doesn't support tuples.

so something like below won't work ,

struct Todo : Codable { 
   var preview : (title: String, subject: String?)
   init(preview: (title: String, subject: String?)) {
      self.preview = preview
   }
}
// ERROR: Type 'Todo' does not conform to protocol 'Decodable'
// ERROR: Type 'Todo' does not conform to protocol 'Encodable'

Would the swift community discuss on ways to support this via a standard delimiter, ? I suggest using the pipe operator ' | ' to accurately depict the notion of tuples.
No, arrays, are not an acceptable way of encoding into JSON because its not reflective of the structure we are trying to represent. Encoding the variables into separate JSON values is also not reflective of the idea on how tuple values are coupled with each other and besides, I see this being a disaster for developers.

If you are for this, along these same lines, certain things that may come up in the discussion are below.

  1. How to support optional variables that are in codable tuples. The mechanism must account for nil tuple values to exist in the edges and of course anywhere in the middle. This I answer in the next point.

  2. Should there be naming? Tuples are anonymous types in swift so the default naming is the order they are initialized and names given if needed. Personally think, there should no naming in the JSON encoded value to keep it lightweight but there of course needs to be a way to track the order. Originally, I mentioned the use of the pipe operator, so it makes sense to use that operator followed by the order the variable is in. So the Todo in JSON would look like below (ignore the implicit unwrapping)

    var todoOne = Todo.init(preview: (title: "Hello World", subject: "Intro"))
    var encodedTodoOne = try? JSONEncoder().encode(todoOne)
    let todoOneString = String.init(data: encodedTodoOne!, encoding: .utf8) 
    // result of TodoOne 
    // { preview: |0 Hello World |1 Intro }
    
    var todoTwo = Todo.init(preview: (title: "Foo Bar", subject: nil))
    var encodedTodoTwo = try? JSONEncoder().encode(todoTwo)
    let todoTwoString = String.init(data: encodedTodoTwo!, encoding: .utf8) 
    // result TodoTwo : 
    // { preview : |0 Foo Bar }
    // ALT: { preview : |0 Foo Bar |1 } 
    
  3. Theres more stuff like custom types in Tuples which I don't think would cause any issues.

If there is support, I can write a full draft that highlights the complete motivation and reasoning beyond what I simply said above.

1 Like

This touches on a much broader topic than just Codable: allowing tuples to conform to protocols. We should focus on that front rather than trying to hack something into the compiler to allow for just Codable synthesis on tuples.

1 Like

Correct me if Im wrong but I believe the core-team wanted Tuples to be anonymous types.

Can they remain anonymous types while still conforming to protocols? My assumption is no because Tuples are generated with gybs. Sorry I'm not too familiar with how gybs work under the hood and need to read more on how LLVM parses swift. Would really love if you have a detailed reference (that goes over compiler theory as well) I can look at from beginner since the LLVM project is so large to start from just reading the documentation.

Yes, the hope is that tuples could conform to protocols in future. This is covered in the generics manifesto as "Extensions of Structural Types".

Tuples are not generated by gyb. They are a part of the language. You may be thinking of the implementation of == for tuples, which does use gyb currently but would not necessarily in some future version of the language.

9 Likes

On a related note, does the Generics Manifesto assume that tuples can have 0, 1, 2 or more elements?

If not, that is if it assumes that single-element tuples continue to be prevented, how would eg the following example from the Generics Manifesto work for functions taking a single argument?

func apply<... Args, Result>(fn: (Args...) -> Result,    // function taking some number of arguments and producing Result
                           args: (Args...)) -> Result {  // tuple of arguments
  return fn(args...)                                     // expand the arguments in the tuple "args" into separate arguments
}

Seems to me that this code example, and especially the comment on the second line, assumes that tuples can have 0, 1, 2 or more elements.

EDIT: Two days later, I've re-asked this question in a separate thread.

I see this discussion is a year old, but I recently ran into this problem myself so hopefully it's OK to revive this. :slight_smile:

Instead of diving into the more open-ended question of whether tuples can/should conform to protocols, can this instead be solved by code generation? The quick solution to the example in the OP is to convert the tuple to a type, which seems to be something that the synthesizer could easily handle. Eg:

struct Todo : Codable { 
   var preview : (title: String, subject: String?)
   init(preview: (title: String, subject: String?)) {
      self.preview = preview
   }
}

the synthesizer could generate:

struct Todo : Codable { 
   var preview : (title: String, subject: String?)
   init(preview: (title: String, subject: String?)) {
      self.preview = preview
   }
   private struct _Preview: Codable {
      let title: String
      let subject: String?
   }
}

and then also generate the code to translate to/from the tuple to the type for decoding and encoding.

As I see it the difficulty with having any standardized way of Tuples implementing Codable is that there isn't an existing standard nor is there a clear (traditional) option to use.

Tuples are not a standard structure so many serializing formats will not have a structure that fits them. Even if you're talking about only JSON we run into problems. A Tuple could be serialized into an object, an array, or a String with the contents. Yes ,we could just pick one, but that decision would be arbitrary and not necessarily translate well to other formats.

Instead, I think the route to go is making it easy do manually. If/when variadic generics drops that should enable some structures that simplify .

Also, one current option would be using a propertyWrapper

@propertyWrapper
struct MyTupleCoding: Codable {
    var wrappedValue: (String, String)

    init(wrappedValue: (String, String)) {
        self.wrappedValue = wrappedValue
    }

    init(from decoder: Decoder) throws {
        let serializedTuple = try String(from: decoder)
        let items = serializedTuple.split(separator: "|")

        guard items.count == 2 else {
            throw DecodingError.valueNotFound(Self.self,  DecodingError.Context(codingPath: decoder.codingPath,
            debugDescription: "Expected \((String, String).self) but could not convert \(serializedTuple) to (String, String)"))
        }
        self.wrappedValue = (String(items[0]), String(items[1]))
    }

    func encode(to encoder: Encoder) throws {
        try "\(wrappedValue.0)|\(wrappedValue.1)".encode(to: encoder)
    }
}

struct MyStruct: Codable {
    @MyTupleCoding var preview: (String, String)
}

let serialized = try! JSONEncoder().encode(MyStruct(preview: ("hi", "there")))
let json = String(data: serialized, encoding: .utf8)!
print(json) // prints {"preview":"hi|there"}
let reversed = try! JSONDecoder().decode(MyStruct.self, from: serialized)
// it works!

That example has a logic error:
What if the data contains β€œ|”?
It throws an error on decode.
At least it doesn’t unknowingly misparse the data, but the data is still ambiguous.

Just using the OP suggestion as the encoding. That's an example, not production code.

Sorry if I'm missing something obvious, but I thought tuples are equivalent to an anonymous record and can always be represented by a struct. Codable synthesis knows how to handle structs, therefore every tuple can also be handled.

A Tuple could be serialized into an object, an array, or a String with the contents.

With what I said above, I'm not sure I follow what you mean. Wouldn't this same issue also be present with serializing the contents of a struct? It's an implementation detail of the decoder/encoder for how to handle containers.

I can't speak to how it's implemented at the to compiler/runtime level.

As far as usage, if by that you mean you could always write an equivalent struct, then sure! but AFAIK at this point there's no way to convert a Tuple automatically to another Type.

According to the docs

A tuple type is a comma-separated list of types, enclosed in parentheses.

My understanding is the current design is conceptually only a generic container. It's intended to be generic and flexible. My concern isn't whether it's possible to synthesize codable, it's what to synthesize.

e.g. if you have var colorHexes (textColorHex: String, backgroundHex: String).There are a lot of ways to serialize that and IMO there isn't one that's "obviously correct".

{
    "colorHexes": "textColorHexValue|backgroundHexValue",
    or
    "colorHexes": {
            "textColorHex": "textColorHexValue",
            "backgroundHex": "backgroundHexValue",
    },
    or
    "textColorHex": "textColorHexValue",
    "backgroundHex": "backgroundHexValue",
    or
    "colorHexes": [
        "textColorHexValue",
        "backgroundHexValue"
    ]
}
1 Like

My understanding is the current design is conceptually only a generic container . It's intended to be generic and flexible. My concern isn't whether it's possible to synthesize codable, it's what to synthesize.

IMHO treating it the same as a struct seems reasonable and practical. I do see your point though.

e.g. if you have var colorHexes (textColorHex: String, backgroundHex: String) .There are a lot of ways to serialize that and IMO there isn't one that's "obviously correct".

But isn't that an implementation detail of the decoder/encoder? If you have:

struct ColorHexes {
  textColorHex: String
  backgroundHex: String
}

you run into the same problem. It's up to the decoder/encoder to interpret containers.

Decoder/Encoder interpret how a structure is serialized. It doesn't define what the structure itself is.

e.g. Decoder has methods for

  • container, e.g. "ColorHexes" : { "Key" : "Value"}`
  • singleValueContainer (e.g. "Key" : "Value"), and
  • unKeyedContainer e.g. "Value"

The implementer of Decodable decides which to use. when finding it's value.

1 Like

I was thinking about a closely related feature a long time ago. I tend to agree with code generation in a very specific sense:

Auto-generating structs and boxing/unboxing code when we declare conformance of a tuple to a protocol.

A straw man syntax and example of what I mean:

extension (Double,Double): Hashable {} 
// Expands to:
struct $Tuple_0_Double_1_Double: Hashable {
    let _0: Double
    let _1: Double
    var $unwrap: (Double, Double) { (_0, _1) } 
    init(_ tuple: (Double, Double)) {
         self._0 = tuple.0
         self._1 = tuple.1
    }
}
// `Hashable` conformance is synthesized for
// `$Tuple_0_Double_1_Double` struct in this example

// At use site:
var gisPlane1: [(Double,Double): Double] = [:]
// becomes: 
var gisPlane: [$Tuple_0_Double_1_Double: Double] = [:]
// Then:
var coordinates = (31.333, 37.557)
gisPlane1[coordinates] = 2.317
// becomes:
var coordinates = (31.333, 37.557) // Stays tuple
gisPlane1[$Tuple_0_Double_1_Double(coordinates)] = 2.317
// and:
for coordinates in gisPlane1.keys {
   print(coordinates)
}
// becomes:
for coordinates in gisPlane1.keys {
   print(coordinates.$unwrap)
}

This feature works for Codable as well as any other protocol. The body of the extension above can define methods and computed properties to implement the protocol and the method body can refer to tuple elements as self.0, self.1 which will become self._0 and self._1 in the generated code above.

I need to work a bit on the interaction between this auto-box/unbox feature and the very important special case of supporting homogenous tuples acting as fixed-sized arrays.

Protocol conformance of any distinct structural type can only be declared once in the scope of a module. Otherwise, the struct will be redeclared. I am assuming (Double, Double) and (x: Double, y: Double) to be distinct, but this is debatable.

1 Like