Protocol Conformance for Tuples / Anonymous Structs

As someone who wants to be able to use tuples to express existing types, I'd want to be able to omit argument labels. I want it to look extremely clean, I'm not just trying to save 4-5 characters. What I really want is something like ExpressibleByArray/String/DictionaryLiteral but for tuples:

protocol ExpressibleByTupleLiteral {
    associatedtype TupleLiteralType
    init(tupleLiteral tuple: Self.TupleLiteralType)
}

class Foo: ExpressibleByTupleLiteral {
    typealias TupleLiteralType = (Int, Int)
    let x: Int
    let y: Int

    init(tupleLiteral tuple: Self.TupleLiteralType) {
        self.x = tuple.0
        self.y = tuple.1
    }
}

let f: Foo = (5, 6)

This is much more flexible than some specific syntax, like .(x: Int, y: Int) and it would work for any type you'd want it to, not just structs. And it's opt-in. As another example of how flexible it could be:

class Person: ExpressibleByTupleLiteral {
    typealias TupleLiteralType = (String, Foo)
    let name: String
    let f: Foo
    let i: Int

    init(tupleLiteral tuple: Self.TupleLiteralType) {
        self.name = tuple.0
        self.foo = tuple.1
        self.i = 5
    }
}

let bob: Person = ("Bob", (5, 6))

Our Person wishes to give i a default value, and we are free to omit it from our TupleLiteralType. Also, since Foo is also ExpressibleByTupleLiteral, we can nest tuples and the inner tuple is inferred as a call to Foo(tupleLiteral: (5, 6)

1 Like

You seem to be misunderstanding. All these would be possible, since you can declare initializers that omit argument labels, and not just structs can have initializers.

It is in fact way more flexible than proposed variations of ExpressibleByTupleLiteral, since you can have defaulted arguments in initializers, and can choose from multiple initializers, whereas the ExpressibleByTupleLiteral requires you to commit to one and only one form of tuple literal. Your person couldn't also be expressible by a tuple including i.

The main difference is the syntax for Tuples isn't distinct. Code in () means a lot of different things whereas [] and "" have very few meanings.

Hmmm. Can you give an example where use of a tuple literal might be ambiguous, or might look like something else? I can't think of any off the top of my head. With or without labels

I really don't like .(...), it feels very foreign, and imo it's a lazy solution. We shouldn't be looking to "compromise," we should first agree on whether something like this should be possible, and if so, find the most ergonomic API. I can't speak for everyone, but I don't want a feature where anyone can just start using .(...) instead of .init(...).

Could we not just make my idea more flexible by removing the associated type requirement? i.e., allow multiple initializers for various tuple types as long as the initializers follow some specific naming pattern?

On the flip-side, it's worth considering that maybe something as strict as only being able to use a specific initializer is not such a bad thing. Things that conform to ExpressibleBy[Collection]Literal are also limited to one initializer. Why should ExpressibleByTupleLiteral be any different? It might make sense if the type author sees one specific use case for tuple initialization. I think the Point use case is a great example. You shouldn't be able to declare a Point with more than one tuple type, nor something like a Rect.

@taylorswift gave an example here: Protocol Conformance for Tuples / Anonymous Structs - #18 by taylorswift

But my point more generally is parens are used for function calls, initializers, scoping/arithmetic, parameter declarations, function declarations...probably more I can't think of off-hand. Not that it's confusing, but compared to square brackets and quotes the case for making it literal-expressible is less clear due to it's much more complex usage graph.

Her example was in reply to this proposal:

It looks to me like you could get this just by dropping the requirement to write the type name before the (...) of an init call.


Not that it's confusing, but compared to square brackets and quotes the case for making it literal-expressible is less clear due to it's much more complex usage graph.

Ah, I see what you mean. However, I'm not sure that holds water since tuples already exist—I can see this argument being made if they didn't exist at all—and my proposal is just a means of using them. This isn't a new use case for parentheses, it's a new use case for tuples. And only tuple literals, at that.

1 Like
let origin: CGPoint = (0, 0)
let size: CGSize = (173, 14)

let rect1: Rect = (origin, size)
let rect2: Rect = (5, 4, 100, 45)

And even CGPoint and CGSize have overloads for different numeric types. It would be odd and inconsistent to support overloaded inits, but not overloaded tuple initialization.

3 Likes

It's adding an additional use case to syntax that already has a lot of use cases. It's less of an obvious win compared to array/dictionary/string literals...I'm unsure of how it should work.

I wouldn't call it odd and inconsistent. Inconvenient, maybe. It's certainly consistent with how the other ExpressibleBy*Literal protocols work. Your example could be modified so that the tuples are the same type for Rect:

let rect1: Rect = (origin, size)
let rect2: Rect = ((5, 4), (100, 45))
1 Like

It's not how all of them work. Have a look at the appendInterpolation methods supported by ExpressibleByStringInterpolation. These are ad-hoc compiler magic.

I'm not sure what you mean, I was only referring to the ExpressibleBy___Literal protocols :sweat_smile:

What is it you're suggesting? I mean, it's all compiler magic isn't it?

This protocol is closely associated with handling of string literals. So what I’m saying is that it wouldn’t be totally unprecedented for a protocol related to literals to have ad-hoc compiler magic involved in its implementation instead of doing everything with associated types and explicit requirements the way we have to in our own code.

this is a pretty artificial limitation of the protocol-driven literals system. there’s no good reason why a type can’t support multiple tuple-literal initializers, and an attribute-driven system like @tupleLiteralConvertible would not have such a limitation. this has nothing to do with variadic generics at all, and could be added tomorrow with minimal disruption to the rest of the user-facing language.

2 Likes

Gotcha! Yeah, that'd be something I'd like to see for whatever this turns out to be. Maybe this:

@taylorswift I agree 100%, was just defending both sides—maybe a bit too strongly. I would much rather have something like my suggestion quoted above, which sounds like what you might want as well.

My main concerns are a) since it's so similar to the existing ExpressibleBy___Literal protocols, I think it makes more sense for the API to resemble that that than to add something entirely new like a tuple init(...) keyword, as was suggested in this thread, and b) I don't want it to only apply to structs, like you seemed to imply here:

at some point we’re going to want to make the literal protocols completely lexical, as described here, and that will require overloaded literal-inits if we want to get all the mileage out of that feature, so i don’t see why @tupleLiteral can’t piggyback on that when it comes time.

// invokes Double.init(sign: .plus, base: .decimal, digits:[9, 8, 9, 1])
let x:Double = 1989

// invokes Double.init(sign: .plus, base: .hexadecimal, 
//                 fraction: [10], digits:[1, 15], exponent: [2, 1])
let y:Double = 0xF1.Ap12

no reason why classes or enums should get left out, im just using structs as an example.

1 Like

I think it would be reasonable to follow the design of the appendInterpolation methods by supporting ad-hoc init(tuple:) initializers. So ExpressibleByTupleLiteral would not have formal requirements and would only exist as a flag to the compiler to look for init(tuple:).

On the other hand, IIRC @Joe_Groff has mentioned that there are some second thoughts about using protocols for the literal support and the core team might want to go in a different direction in the future.

3 Likes

Did a proposal ever come from this?

I would really like to see tuples be able to conform to protocols. Seems to me most of the objections could be gotten around by requiring that the protocol conformance must be declared on a extension to a typealias that refers to the tuple in question. That way the scope of the conformance would be limited to the scope of the typealias.

The reason I'd like this feature is because, lets say I have:

protocol X {
    var a: Int { get }
    var b: Int { get }
}

struct GeneratedProtobufStruct {
    var value: Int = 0
}

Now, I want to make GeneratedProtobufStruct conform to X but it can't conform because it only has one stored property, and I can't add another stored property to it in an extension. Further, I don't want to make a struct that contains a GeneratedProtobufStruct because it's wasteful to have to copy all its data just so I can conform to a protocol.

The best solution would be to simply do:

typealias WonderfulTuple = (GeneratedProtobufStruct, Int)

extension WonderfulTuple: X {
    var a: Int { self.0.value }
    var b: Int { self.1}
}

That's not how typealiases work in swift

// test.swift
struct Foo { }
fileprivate typealias Alias = Foo
extension Alias {
  func f() { print("Hello") }
}
// main.swift
Foo().f() // prints "Hello"

SE-0283 Tuples Conform to Equatable, Comparable, and Hashable got accepted

1 Like