[Pitch] One-Element Tuples
- Proposal: SE-NNNN
- Authors: Slava Pestov
- Review Manager: TBD
- Status: Implemented with -enable-experimental-variadic-generics staging flag
Introduction
This pitch lifts the artificial restriction preventing one-element tuple types from being written and constructed.
Motivation
As a generic container storing a single value, one-element tuples are not very useful on their own, so today the compiler does not allow them to be written or constructed.
Labeled one-element tuples, such as (element: Int)
, are diagnosed with a post-pass over the type checked syntax tree.
Unlabeled one-element tuples cannot be written at all. In type context, (T)
resolves to the same type as T
, allowing users to make a stylistic choice between (Int) -> (Int)
and (Int) -> Int
for example. In expression context, (expr)
is the same as expr
, since parentheses are used for grouping expressions, disambiguating infix operator precedence, and so on.
However, we expect that one-element tuples might naturally arise as follows. Once Swift supports variadic generics, it will be possible to define a function which returns a tuple type containing a variadic pack expansion:
func makeTuple<T...>(_ elements: T...) -> (T...) { return (elements...) }
This creates a conundrum: what is the result type of calling such a function with a single element, like makeTuple(1)
? While the behavior most consistent with the existing language model would assign this expression the type Int
, such “implicit unwrapping” behavior would introduce certain conceptual difficulties, which are further elaborated in the Alternatives considered section below.
Instead, we propose lifting the restriction on writing one-element tuple types in Swift, so that the return type of makeTuple(1)
becomes the one-element tuple type with element Int
.
Proposed solution
We propose the formal introduction of both labeled and unlabeled one-element tuple types in the language.
A labeled one-element tuple type already has a spelling, (label: element)
. This proposal would simply remove the existing diagnostic that is produced when such a type is resolved.
On the other hand, an unlabeled one-element tuple type cannot use the spelling (T)
, since that would be ambiguous with the existing parentheses sugar in type context, and expression grouping syntax in expression context. Instead, we propose the syntax (_: element)
for an unlabeled one-element tuple type.
We do not expect one-element tuples to be a widely used idiom on their own; they will mostly exist to make the language model more consistent with the introduction of variadic generics. However, being a separable feature, they deserve their own proposal.
Detailed design
In type context, (label: T)
resolves to a one-element tuple type with label label
and element type T
. In expression context, (label: expr)
evaluates to a one-element tuple with label label
and element value equal to the result of evaluating expr
.
Similarly, in type context, (_: T)
resolves to an unlabeled one-element tuple type. In expression context, (_: expr)
evaluates to an unlabeled one-element with element value equal to the result of evaluating expr
.
Swift supports various implicit conversions involving tuple types; these generalize to one-element tuples as follows:
-
Label elimination. An expression of type
(label: T)
implicitly converts to a value of type(_: T)
. -
Label introduction. An expression of type
(_: T)
implicitly converts to a value of type(label: T)
. -
Label permutation. Tuple types with more than one element can be permuted; for example,
(a: T, b: U)
implicitly converts to(b: U, a: T)
. There is no equivalent with one-element tuples, since a singleton set only admits the identity permutation.
Swift also supports tuple projection expressions; these generalize to one-element tuples as follows:
-
Index projection. If
expr
has type(label: T)
or(_: T)
, thenexpr.0
unwraps the labeled or unlabeled one-element tuple; the result has typeT
. -
Label projection. If
expr
has type(label: T)
, thenexpr.label
unwraps the labeled one-element tuple; the result has typeT
.
However, we are not proposing introducing any new implicit conversions between one-element tuples and their corresponding element type:
- An expression of type
(label: T)
does not implicitly convert to a value of typeT
or vice versa. - An expression of type
(_: T)
does not implicitly convert to a value of typeT
or vice versa.
Source compatibility
In Swift 5.6, the type (_: T)
resolves to T
and the expression (_: expr)
evaluates to expr
. This proposal breaks source compatibility with code that uses these forms. We strongly suspect there are little to no projects making use of this syntax, but we wish to call it out as a potential incompatibility.
Effect on ABI stability
This proposal is additive, and has no effect on the existing ABI of libraries. The existing Swift runtime entry point for constructing tuple type metadata supports one-element tuple types already, so there are no backward deployment concerns with code that makes use of this feature.
Effect on API resilience
This proposal is additive, and has no effect on the existing API of libraries.
Alternatives considered
Allow implicit conversion between one-element tuples and their element type
We could allow the implicit conversions that are explicitly called out as not supported in the Detailed design section above. While this is probably not too difficult to achieve in principle, we generally favor not introducing new implicit conversions by default.
A different syntax for unlabeled one-element tuples
We chose (_: elt)
for unlabeled one-element tuples since it is an existing production in the Swift grammar, in both type and expression context. However, it does break source compatibility in a narrow edge case, as mentioned above in the Source compatibility section.
One alternative is to take inspiration from Python and use a trailing comma, so that (elt,)
builds an unlabeled one-element tuple. The form (_: elt)
has the advantage that it evokes the existing tuple syntax, whereas the trailing comma form (elt,)
is potentially more confusing. The trailing comma still has the disadvantage that it is theoretically source-breaking, and even worse, it is inconsistent with existing usage of the trailing comma in Swift, which is a no-op in all other contexts where it is valid to write.
We could instead adopt a syntax which is wholly new, such as @tuple (elt)
or something even more bizarre. However, this increases the surface area of the language in a new way for what will surely be a rarely-used feature.
Don’t introduce one-element tuples at all
It may be possible to achieve the goals of the variadic generics feature without introducing one-element tuples as a visible language feature, for example by always introducing index projections in the right places. However, this seems difficult to do in a logically consistent way. For example, consider the following code:
struct G<T> {}
func makeGTuple<U...>(_ elements: U...) -> G<(U...)> {
return G<(U...)>()
}
Unlike makeTuple()
shown in the Motivation section, which returns a tuple type, the makeGTuple()
function returns a generic nominal type instantiated with a tuple type. Today, generic nominal types are invariant in Swift, meaning two instantiations of the same generic type with different generic arguments are completely unrelated types in the implicit conversion lattice. To effectively hide one-element tuples from the user, the type G<(_: Int)>
would need to implicitly convert to G<Int>
and vice versa, introducing an entirely new class of implicit conversion. When a type has multiple generic arguments, this becomes even more fraught; every possible combination of generic argument wrapped or unwrapped would need to convert back and forth to every other possible combination.
We believe that simply lifting the existing artificial restriction on one-element tuple types will result in a better language design overall.