The Generics Manifesto and single-element tuples

The 1-tuple exists, but it isn't usefully different from the single element type.

it’d be more useful if we had tuple splatting for variadic arguments tho

I didn't say that the lack of unlabeled 1-tuples is the cause, I said that the underlying problem is

I've been bumping into and reporting tuple- and parentheses related bugs since the first versions of Swift, and its not just me. Here is a recent bug that @mattrips reported and just started working on, it has some interesting comments.

Also, the following is another example of the confusion caused by 1-tuples having the same type as their element:

But the language reference says that 1-tuple types don't exist:

All tuple types contain two or more types, except for Void which is a type alias for the empty tuple type, ().

It also says:

In Swift, there are two kinds of types: named types and compound types. A named type is a type that can be given a particular name when it’s defined. Named types include classes, structures, enumerations, and protocols.
/.../
A compound type is a type without a name, defined in the Swift language itself. There are two compound types: function types and tuple types.
/.../
You can put parentheses around a named type or a compound type. However, adding parentheses around a type doesn’t have any effect. For example, (Int) is equivalent to Int.

I would interpret this as 1-tuples not being a thing, they don't exist, in any way. And (Int) == Int is a "named type", not a tuple type.

But if, as you say, 1-tuples do exist, and are equivalent to their element's type, because (T) == T, then all "named and compound types" are "compound types", which sounds strange.

I'm afraid I'm still no wiser than when I wrote:


I'd like to see a code example illustrating the following:

Because as far as I can see, the opposite is true: Special casing 1-tuples spreads through all things dealing with / affected by 1-tuples, as eg variadic functions in their unary form. Here is an example of a user who is annoyed by the special casing of 1-tuples.

I wish Swift could have more consistent (ie fewer and simpler) rules in this area. And the only reason I can see for why tuples can't simply have zero or more elements and why the same rules can't apply to all tuple types, is the conflation-of-parentheses-syntax-issue, without which I doubt anyone would find it natural to consider the type of a single element tuple equivalent to the type of its element.

I'd still say that the current rules are complex and confusing (for both users and compiler devs):

  • Tuple types can have 0, not 1, or 2 or more elements, but it seems like 1-tuples also exist, in some way.

  • It seems like all types are tuple types (because (T) is equivalent to T) but I'm not sure.

  • To me, saying that there exist single element tuples and that their type is equivalent to the type of their (single) element, and that the language prevents us from naming or accessing their single element or otherwise treating them like all other tuple types, sounds almost as strange as if for example enum MyEnum { case singleCase(String) } would not be allowed because it has just a single case, although single-case enums would still exist, in a way, and their type would always be equivalent to the type of the associated value of their single case (provided it has an associated value of course). Or, if Swift had fixed size array types, with a type level count, if single element fixed size arrays would be considered unnecessary and therefore although they would kind of exist, their type would be equivalent to their single element's type, and we would not be allowed to access its single element in the same way as we access all other fixed size array types (of sizes 0 or 2 or more).

  • There has always been and still are lots of tuple- and parentheses- related compiler bugs. My feeling is that I'm no longer sure whether reporting and "fixing" these bugs simplifies or complicates the situation.

2 Likes

These kinds of issues are with the function type representation, primarily. If function calls represented their arguments as a list of expressions instead of sometimes as a tuple, which better reflects the language model, then 1-tuples would not be an issue in situations like this.

1 Like

The linked example would work fine in the model as I've described it. a is contextually of type UnsafeMutablePointer<Int> so the single-value reallocate would be chosen, parens or not, tuple or not.

For another example, let's say we had a variadic dual to print to read a bunch of values from stdin:

func scan<T...>(_ types: T.Type...) -> (T...)

If you called scan(Int.self), you would expect a single Int back, not a tuple.

1 Like

Thanks again, I really appreciate you taking the time to answer these questions.

I mentioned that bug as an example because of the conversation (between @mattrips and @beccadax) in its comments, eg:

I'm afraid that I'd be putting a band-aid on a small symptom of a larger bug. The code that folds (T) is doing something that is fundamentally illegal.
It is creating a single-element tuple, but there is no such thing as a single-element tuple. The documentation states that, "All tuple types contain two or more types, except for Void which is a type alias for the empty tuple type, ()."

Internally, Swift actually does use single-element tuples purposefully in some cases (mainly for single labeled arguments). But we know they sneak through sometimes when they shouldn't. The set of refactorings needed to fix it properly is too big to do immediately, but several of us are gearing up to wage war on the root causes of these problems.
So apply your band-aid and bide your time. Honestly, if it actually fixes a single-element tuple instead of just limiting the damage, it's better than the tuple band-aid I wrote a couple weeks ago.

And also because of the content of the reporter's (also assignee's) related post here.


You're right, but I'd prefer if the implicit splat wasn't there (and thus that the 1-tuple's type was separate from its element), since it would allow me to eg write extensions on all tuple types for which all (0 or more) elements satisfies some constraint etc. But as shown above, the current special casing of 1-tuples makes that complicated or impossible in the general case (the current special casing results in the need for further special casing). It would be interesting to hear if @Torust has any thoughts about this feedback.

I see your point, but again, I still don't like the special casing of 1-tuples (having an implicit tuple splat), even here, because it's trading consistency (and the power/simplicity it implies) for mild convenience in a specific case (by introducing an inconsistency and the trouble/complexity it implies).

I'm not sure if it's interesting or not to contemplate the behavior of the following depending on if 1-tuples existed as separate types from their element's type or not:

func foo<...T>(_ types: T.Type...) {
    print(scan(types))
    // Or would it be:
    // print(scan(T...))
    // ?
}

I'd be happy to come to the conclusion that my worries and/or preferences are founded on false premises, but I'm afraid I seem to need a lot of explanation in order to arrive at that conclusion.
: )

1 Like

[Not necessarily directed at you, Joe] For me, the question still stands: As a matter of formality, why doesn't the single-element tuple exist in Swift?

And, by "in Swift", I mean at the source-code/user level, not internal to the compiler. Regarding that distinction, please see SR-8172.

1 Like

Likewise, thanks for having patience with my explanations. I know there's a lot of implementation mess and technical debt to fight through that's obscuring the model Swift really intends to implement.

This is where the difference in generic context comes into play. A single type can be both itself in a nongeneric context and a list of one type in a variadic generic one. In the body of foo, we know types could have any number of elements, so it would always be worked with as a tuple.

:+1:

I agree.

1. Removing exceptions has a value by itself, and can often add power to the language by simplifying its components while at the same time allowing for better compositionality.

2. Introducing exceptions motivated only by small wins in specific use cases, in acts of unexamined pseudo-pragmatism, probably always adds more complexity and confusion than power to the language.

I guess I'm worried that the issues at hand (implicit tuple splat of the existing-yet-non-existing-1-tuples, the implication that all types are tuple types, uncertainty on how and when various different cases of variadic generics should be special cased, messy implementation, etc.) might be symptoms of Swift Evolution having been doing too much of 2 and too little of 1 (on top of conflating different concepts all dressed up as parentheses).

I also guess that many will dismiss this view as naive idealism or, if I manage to conceal my lack of actual knowledge well enough, too academic.


Yes, and AFAICS both variants of the print statement would be valid code, as would this:

func foo<...T>(_ types: T.Type...) {
    print("A:", scan(types))
    print("B:", scan(T...))
}

It would be interesting to see the behavior of eg

foo()
foo(Int.self)
foo(Int.self, Bool.self)
foo(Int.self, Bool.self, String.self)

for these two different models for tuples:

  • The type of a 1-tuple is equivalent to the type of its element (the current/intended one).

vs

  • The type of a 1-tuple is separate from the type of its element, just like the type of any other N-tuple (the IMO more consistent model that I'd prefer, assuming solved conflated-parens-issue).

I'm too tired to try and figure that out though.

I'm pretty sure this discussion is grounded in the real world. In fact, this thread started with Generic Manifesto's proposed real-world use of single-element tuples. Consider:

2+2, 3+1, 10-6, and 4, each can be equated to the same value, but each models a different real-world event. They are different things.

T and (T), each can be equated to the same MetaType, but each models a different real-programming-world event. They are different things.

I don't recall any of these matters being decided by evolution proposals, so I think your blame might be misplaced here. There were proposals to try to deal with certain aspects around tuples, particularly SE-0029 and SE-0110, but moving away from modelling parameter lists as tuples, banning single-element tuples, etc were all done outside of, and generally before, the evolution process. I remember that one of the earliest Swift bugs that I reported, before it was an open source project, was that the removal of labelled single-element tuples broke the ability to label enum cases with a single piece of associated data.

2 Likes

Right, I should have written just "Swift" instead of "Swift Evolution" (but I'd still be happy to see Swift Evolution doing a bit more of 1).

Not super important, but ExpressibleByTupleLiteral and a library level way to destructure values into tuples would be "nice to have" for Python interop.

-Chris

5 Likes

To add on to that, if we remodel tuples in terms of a specially-treated Tuple<T...> type, we would have a natural syntax for expressing extensions and conditional conformances on tuples:

extension Tuple<T...>: Equatable where T: Equatable { ...

This absolves the need to define new grammar to support (T...) in the extension declaration.

That’s not actually the syntax we use for extensions on generic types. Do you mean extension Tuple: Equatable?

1 Like

Ah, I wasn't clear. I was presupposing the implementation of the syntax at the end of the section on Parameterized Extensions. Although, I didn't quite get that right, either :stuck_out_tongue:

I'm not sure how such a model would handle the case where T... is an empty list, ie could there be an empty Tuple<> that is different from Tuple<Void>?

This problem does not exist in the current ~corresponding formulation from the generics manifesto:

extension<...Elements : Equatable> (Elements...) : Equatable {   // extending the tuple type "(Elements...)" to be Equatable
} 

(Disregarding the question about whether all the elements of an empty list of types could be considered as conforming to Equatable.)

And I guess, we'd also have to ask ourselves whether we should special case single element Tuple<T> types or not, that is: If eg the type Tuple<Int> should be allowed and/or be equivalent to Int.

IMO the obvious answer is that there should be no special casing, ie the type Tuple<Int> should be allowed and not be equivalent to Int.

It is for the same reason that I think Swift should not special case single element tuple types like it currently does. However, that would require fixing the parentheses issue (the conflation of different concepts caused by overloaded parentheses).

I think the type should be Tuple<...T> : ExpressibleByTupleLiteral instead of Tuple<T...>. The only issue I see is the empty tuple which would require some special casing. ExpressibleByTupleLiteral would always produce a tuple of at least one element. Furthermore we'd need variadic optional labels for each type.

Bikeshedding:

struct Tuple<...label?: ...T> : ExpressibleByTupleLiteral

The extension can be very similar:

extension Tuple : Equatable where ...T : Equatable { ... }

We can also drop the following special case T == (T) or T == Tuple<T> and make it T != Tuple<T>. Furthermore we can easily have Tuple<label: T> types or if we really want (label: T).

@Jens, to clear the way for these sorts of potential improvements and others, perhaps it is time to start formulating a pitch to revise the definition of the tuple type.

1 Like