Pitch: User-defined tuple conformances

Also, tangentially, if you try to force the creation of a single-element tuple by naming the contained element, e.g.:

func foo() -> (result: String) {
    return ("hello!")
}

…the resulting compiler error is a bit misleading, as it implies you can have single-element tuples, just not with named elements.

Cannot create a single-element tuple with an element label

(with a fixit to just remove the name, but not the misleading parenthesis)

Reinforcing my point that single-element tuples, or rather Swift's aversion to them, is already odd. (I'm not critiquing it, just noting - I suspect there's an upside to disallowing them, such as removing confusion between tuple declarations and mere scoping, both of which use parenthesis… although they could be distinguishable by requiring single-element tuples to have a trailing comma)

1 Like

The current behavior is consistent, in that the parentheses are just grouping and not tuple formation -- so (123) has the exact same type as 123, which is not a tuple type. All of the funny behaviors of tuples immediately follow from this syntactic overlap (and the subsequent decision to disallow one-element tuples instead of having a special (x,) or (_: x) syntax or whatever that has a distinct type from x).

4 Likes

There’s some very informative historical discussion in this pitch for single-element tuples from last year.

3 Likes

For the sake of argument, another alternative would be to say that tuple members are suppressed from being looked up on types that appear to be scalar. We had the problem you describe with the .0 member back in the Swift 0.x days, where it would be present on every type since every type is a single-element tuple of itself, acting like the identity member because the first element of a 1-tuple is the entire value. Now .0 is only resolvable when a type is visibly a tuple in context.

7 Likes

Is there a rationale somewhere to read about why a single-element tuple must be identical to the element at all?

Joe provides that in the form of a counter-argument in the above linked pitch thread.

1 Like

This is a good justification to draw the line where you’ve drawn it.

4 Likes

I hope that it is ok that I bump this thread.
I was just curious about the state of the pitch. The possibilities it brings are really, really cool! :blush:

Not much progress since the pitch sadly. The current state of the implementation is that the tuple conformance mechanism is already used for Copyable and Sendable tuples, and user-defined conformances make it as far as SILGen. The IRGen and runtime component still need to be implemented. If anyone wants to jump in I’d be happy to help figure it out.

6 Likes

Will this be possible:

protocol P { func foo() }

extension (Int, String) : P {
    func foo() { /*...*/ }
}

(1, "hello").foo()

or what syntax will it be to express the above?

Or simply:

func (Int, String).foo() { }

to have the "declaration site closely resembling the use site".

This if there's no need to state the protocol conformance.

I remember seeing the idea some time ago on this forum of making this form:

func T.foo() {}

an equivalent short hand notation of a longer form:

extension T {
	func foo() {}
}

IIRC that idea (can't find the thread right now) got quite a few positive replies but wasn't explored any further.

The pitch only allows for (repeat each T) to conform to P where T: P, but with a bit of work we could also support more general tuple conformances.

A tuple conformance is essentially like an extension of a variadic nominal type:

struct FakeTuple<repeat each T> {}
extension FakeTuple ...

It's not supported right now, but we want this to work for parameter packs in general:

extension FakeTuple where (repeat each T) == (Int, String) {...}

So once that's worked out, your tuple conformance falls out immediately.

We have to be careful if we generalize this to avoid logical inconsistencies with one-element tuples; because a tuple conformance is just like the FakeTuple above, except that FakeTuple<T> is actually just T when T is a scalar type. We don’t want to back door a mechanism to make an arbitrary type conform to P because it was a one-element tuple before substitution.

You also wouldn’t be able to define two distinct tuple conformances to the same protocol, even with distinct requirements, because this would violate the restriction on overlapping conformances, just as if you tried the same with FakeTuple.

Also while it doesn’t create theoretical difficulties I’d prefer if we draw the line at adding arbitrary members to tuple types, and limit this to protocol conformances only. Otherwise overload resolution gets more complicated.

In that world, what's the rule for who's allowed to create tuple conformances to a protocol? For named types, we say it must be the module that either declares the type or declares the protocol. How does that work with tuples? Nobody owns a tuple type declaration.

The structural types notionally belong to the standard library, so only the standard library can non-@retroactive-ly define conformances for standard library protocols, and only the protocol's home module can non-@retroactive-ly define tuple conformances for a protocol outside of the standard library.

8 Likes

Yeah, basically the author of a protocol gets to decide if tuples conform or not, and nobody else.

4 Likes

Is there a way to take this on a test drive in an available toolchain? I tried

      swiftSettings: [
        .enableUpcomingFeature("TupleConformances")
      ]
      swiftSettings: [
        .enableExperimentalFeature("TupleConformances")
      ]

in Xcode 16.1 beta 1 and neither seems to get the following code to work:

public protocol TestProtocol {
  
}

extension Tuple: TestProtocol where repeat each Element: TestProtocol {
  
}

extension <each T> (repeat each Element): TestProtocol where repeat each Element: TestProtocol {
  
}
1 Like

Unfortunately it's not fully implemented:

  1. There's no IRGen or runtime support. I had some experimental code a while ago to stub out enough in IRGen to get it working, but this approach didn't backward deploy at all, requiring a runtime change, and it would be nice if we could get the stdlib tuple conformances to backward deploy, at least when you don't use dynamic casts.
  2. Also, non-static methods in tuple conformances that actually pass a tuple as the self value hit some existing SILGen bugs with parameter packs, so don't get past type checking.

The only syntax that works is extension Tuple,but you need to actually declare typealias Tuple<each Element> = (repeat each Element). It won't get far though, as per the above.

2 Likes

Thanks for the update. Been playing with parameter packs recently and this would be a very welcome improvement!

2 Likes