Single-element labeled tuples?

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.

Sure, that's true, and perhaps subtyping is the wrong way to think about these.

Swift's tuple type is really a mix of a traditional tuple type (ordered, indexed by ordinal, no subtyping) and record type (labeled, potentially shuffleable, often with subtyping rules that allow for shuffling as well as adding new labeled elements).

I'm not sure this is a great analogy, if only because IUOs were conceptually removed from the type system in SE-0054, and literally removed from the type system in master in the last month or so. The remaining Optional<T> type definitely has a transitive subtyping relationship. If you can assign something declared as U to T, you can also assign that U-typed value to something declared as T? or T! because the subtyping relationship for T? is also covariant, i.e.:

  U <: U?
  U? <: T? if U <: T

The only interesting/unique behavior is that per SE-0054, if an expression fails to type check when treating a value declared with ! as if it were Optional<T>, we'll attempt to implicitly force the optional and see if it now type checks. This means that you can assign something declared as T! to something declared as T.

2 Likes

Indeed: the implicit unwrapping behavior of T!, whether they're a distinct part of the type system or (now) not. I mention the analogy because differently labeled tuples, like T!, aren't formally distinct in the type system but do have implicit ways of relating to one other which can't entirely be explained by subtyping.

sometimes I find myself doing that...

var barbar: (option1: String, ())

But then. I convince myself it is ugly and take it out.

3 Likes

Before I forget that thought. A moment ago I wanted a typealias for a two value tuple but where one could still choose a label.

Bikeshedding:

typealias SpecialTuple<T> = (identifier: String, T)

// Using a single-element labeled tuple to merge the label into the other tuple
SpecialTuple<(name: String)> == (identifier: String, name: String)