Single-element labeled tuples?

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.

There is no answer to this question. Swift had one model but moved away from it, but not completely. But labels are currently a part of the tuple type, if that’s what you’re asking.

…only if you try to conceptualize the relationship between these types as a subtyping relation. Unless I’m mistaken, they don’t formally have such a relationship in Swift.

I would draw an analogy between the relationship between labeled tuples and unlabeled tuples to that between IUOs and their wrapped types, a special relation that isn’t quite transitive when it comes to assignment, but with behavior that’s meant to serve certain pragmatic purposes.

As to what relationship these types ought to have, I wonder if this is essentially a newtype relationship we’re trying to define. Certainly, in Haskell, newtypes can’t be mixed with the existing types from which they derive, but it wouldn’t be right out of the question to have a different design. Given newtype B = A; newtype C = A, one could envision a language where it’s possible to assign a value of type A to a variable of type B and vice versa, A to C and vice versa, but not B to C or vice versa.

Terms of Service

Privacy Policy

Cookie Policy