Single-element labeled tuples?

Yes, I fully understand and support usage as such, though in this case I’d be more inclined to fixed-size arrays that have been discussed on this forum a few times - i.e. something link var data: Int[2]… Note that I’ve never said that tuples should not be used at all, nor have I said that I’m against single-element tuples - just that the examples raised here did not convince me about the importance of them.

@Michael_Ilseman - sorry, I missed that part, which is quite important in your argument. My apologies. I kind of see the point in using the single-element tuple here.

Since we can have n length tuples, including 0 length, it is weird that a length of 1 is excluded. I vote to include this.

5 Likes

I agree, as a humble end user without knowing anything about the compiler.

It seems like a weird special-case that spreads ripples of complexity through the implementation of the compiler and, if so, that should imho outweigh any concerns about possible use cases for labeled single-element tuples, and the question should perhaps rather (have) be(en): Are special casing labeled single (and zero?) element tuples worth the complexity?

If it would simplify the implementation (and the concept of tuples), I would even think labeled zero-element tuples would be fine, even if nobody would ever use them.

It would of course be more interesting to hear what people with insight into programming language theory had to say about this

I’m also keen to see this feature, for both the @discardableResult case, and tuple values which are to be extended in the future.

Rather than restore the feature exactly as it was, I’d advocate keeping single-element tuples distinct from the raw type.

This would prevent this questionable feature: value.0.0.0.0; and presumably eliminate a source of compiler bugs.


To enumerate the possibilities:

//disabled - no `anyValue.0`
let tuple = 1 //clearly no intent to cast
let tuple = (1) //collides with regular brackets
let tuple = 1.0 //I don't even…

//allowed (?)
let i: Int = tuple //1, 2b
let tuple = 1 as (label: Int) //1, 2a
let tuple: (label: Int) = 1 //1, 2a
let tuple = (label: 1) //1, 2(a|b|ab)
//allowed (3)
let i: Int = tuple.label
func tupleReturning() -> (label: Int) {return 1}
func tupleReturning() -> (label: Int) {return (label: 1)}
  1. Allow all use cases, and simply disable the equivalence to the raw type (which is out of the question at this stage anyway).

  2. Re-enable single-element tuples as a separate type, with one/both restrictions:
    a. require accessing .label to convert (label: Int) to Int
    b. require (label: 1), by disabling conversion through type inteference.

  3. Re-enable only the last set of use-cases, or a subset with restrictions a/b, and restrict construction of single-element tuples to @discardableResult functions. The more conservative, but restrictive, approach.

Personally, I’m a fan of 2a, as it works nicely with use in discardable functions, and isn’t overly restrictive.

3 Likes

Seems self-evident to me that single-element tuples are a good thing.

Because of the label, its more useful than a one item array. I wouldn’t want an array where the count can’t be one. I don’t like messing around with my code to accommodate surprising rules about tuples either. It’s ugly, and it’s likely more cognitive load for new Swift devs than confusion over the parens would be.

5 Likes

They were banned because the “classic” function-argument-tuple model and tuple conversion rules caused type system problems, particularly because every T was implicitly convertible to (label: T) and back. We’ve since shored up the argument label model, and various people have discussed limiting the tuple conversion rules to be less unpredictable and less straining on the type checker. If we solve that problem, then there’s no technical reason remaining for single-element tuples to be banned.

7 Likes

What sort of limitations are we talking about? Is implicit conversion in a particular direction a source of problems, or both?

Right now, Swift’s type system allows tuple conversions that both add and remove labels. These kinds of bidirectional subtyping rules are difficult for the type checker because it isn’t clear which one is “better” or more specific. If we made the rule directional, to say that maybe (T, U) is a supertype of (label: T, label: U) and not the other way around, then the type checker would be able to clearly favor labeled tuples as more specific than unlabeled ones.

14 Likes

So, we can have implicit conversion in one direction (IE: to the supertype), but not both? I imagine this works similarly to the conversion between raw types and labelled 1-tuples (assuming we ban unlabelled 1-tuples), so we can discount option 1 from my post above.

Given I support 2a, I’m quite happy with that.

Is this encoded correctly in the ABI? I recall we used to have issues with ambiguous mangling surrounding function types taking tuples.

AFAIK, the mangling now models functions as having multiple arguments instead of a single tuple.

2 Likes

This would also be massively source-breaking, no?

It would definitely be source-breaking. We’d have to do some investigation to see how widespread the breakage is.

1 Like

Yes, @xedin committed patches to mangle as a list of label/type.

1 Like

I’ve been planning on doing that investigation as part of some implementation work in the type checker.

What we have today is just fundamentally broken:

let x = (a: 1, b: 2)
let z1: (c: Int, d: Int) = x // cannot convert value of type '(a: Int, b: Int)' to specified type '(c: Int, d: Int)'
let y: (Int, Int) = x  // works: makes sense
let z2: (c: Int, d: Int) = y  // works: uh, what?
3 Likes

This was deliberate and has been discussed on this list.

Since Swift still supports reordering tuples–for example, let foo: (y: Int, x: Int) = (x: 1, y: 2)–we allow stripping or adding of labels, but not discordant assignment. (This was quite elegant when argument lists were still tuples and labeled arguments could be supplied in any order. But we’ve steadily chipped away at that design.)

1 Like

Yes I'm aware of some past discussion, and the choice may have been deliberate, but that doesn't mean it's not fundamentally broken.

By fundamentally broken I mean that the subtyping relation is transitive, yet you immediately reach a contradiction here:

typealias S = (a: Int, b: Int)
typealias T = (c: Int, d: Int)
typealias U = (Int, Int)

var s: S = (a: 1, b: 2)
var t: T = (c: 1, d: 2)
var u: U = (1, 2)

// read X <: Y as X is a subtype of Y
u = s // S <: U? yes
u = t // T <: U? yes
t = s // S <: T? no
s = t // T <: S? no
s = u // U <: S? yes; T <: U and U <: S implies T <: S
// ... but we just said that T is not in fact a subtype of S...

There is no need to allow arbitrary addition of labels in order to allow reordering. You can have a well defined subtyping relation that says all shuffles of labeled records are subtypes of one another, i.e. fundamentally the same type.

3 Likes

Not sure if this muddles or clarifies the discussion, but we can think of unlabeled tuples to be implicitly labeled by the argument’s ordinal number. This is already true when accessing the tuple’s elements.

On a limb: the transition from labeled tuple to unlabeled and back is some form of type erasure?

I have been puzzled by the implicit tuple conversions before… unsure whether to think of labels as part of the type or not.

What @rudkx said, means that explicitly labeled tuples can be thought of as unordered type, conversation between them are done by record names.

When unlabeled tuple enters the conversion, the labeled tuple experiences “name erasure”, explicit labels are replaced with implicit labels - ordinal numbers based on the order of definition.

I’m still confused what this implies about subtyping.

I now think the above statement has it backwards. Tuples are fundamentally an ordered type, labels provide only syntactic sugar for semantic clarification when accessing elements and enable automatic element reshuffling when assigning to different tuple type with matching labels.

Terms of Service

Privacy Policy

Cookie Policy