Roadmap for and state of single element tuple types?

Regarding syntax:
I neither like (1). (maybe due to memories of Pascal ;-), nor (1,) (trailing commas just look odd).
My preference would be (:1) - if there shouldn't be a label, just leave it out.

But afaics, there would be no ambiguity on the declaration site, and as single element unlabeled tuples probably won't be used often (after all, we don't actually need them ;-), I could live without any special syntax

let x = (5) // this is just an int
let y: (Int) = (5) // this is a tuple

What about:

let x = (5) as (Int) // ?
let y = 5 as (Int) // ???

And maybe

let z: (Int, Int, Int) = 1, 2, 3

?

This opinion might be an exception in this discussion, but I wouldn't mind restoring the old model, where unlabelled 1-tuples are equivalent to the type ((Int) === Int), with the difference that the contraversial .0 member (which just returns self) is disabled.

(Or perhaps we could keep .0: there was recent discussion about finding an identity keypath, this could be a good candidate, since it can't be shadowed)

This model nicely avoids the semantic issue of bracketing vs tuple creation, since (4 + 4) * 2 defines the same order of operations, and is semantically correct, for either mental model.

Given the new conversion rules noted in the opening post, I would expect this to be compiler friendly, and it seems user-friendly, too.

I'm simply referring to the original system that's diverged only due to the need for function parameter attributes that couldn't be represented in tuples themselves. But you're already aware of this so I won't elaborate over it too much. But even though it's diverged a little in one place it's still prevalent everywhere else.

Yes, exactly, this is the behaviour I'm advocating for (which is the current one).


No there wouldn't be. But we don't have "⟪⟫" or something else and there's nothing we can do about that now. We have parenthesis and the current behaviour is the most consistent behaviour we can have due to this IMO. See the relation between how expressions are reduced and how tuples are as well.

(5 + 5) * 2 == 20 // ('Int' + 'Int') * 'Int' == 'Int'
((5 + 5) * 2, (10)) == (20, 10)  // (('Int' + 'Int') * 'Int', ('Int')) == ('Int', 'Int')

Note, I only meant for expression evaluation and type inference, sorry if I wasn't clear.


I don't see a problem here. You could also do this with structs or classes:

let t = Box(label: 12).label + Box(label: 34).label
print(t) // 46

let t = (x: ..., x: ...) should be an ambiguity bug IMO, regardless of being able to use .# as mentioned in the other thread.

let (a, x: b) = t would not be a problem if the above is fixed as it is only a consequence.


I think the fact that compiler crashes should be enough to say this is a bug :slight_smile:. I imagine the program shouldn't compile when .true is droppped. Since (true: Bool) != Bool and ! is not defined for it.


Most of the inconsistencies you mention are with dealing with labelled single element tuples. They are in my opinion are just bugs that need fixing.

2 Likes

Perhaps these particular examples are, but there are many other bugs and unfortunate circumstances (like the above example about the effect implicit construction/deconstruction/conversion or same-ness of ⟪T⟫ and T will have on variadic generics, enum with associated values, pattern matching, etc, etc), which have probably more to do with the layers of confusion resulting from having parentheses play all these multiple roles.

But ... if we accept that ⟪Int⟫ is the same as Int, then why isnt't Box<Int> the same type as Int? Why should generic structs or classes be allowed to have a single type parameter, aren't they product types too?
: )

Also, let's remember that the following two examples (which you have no problem with) are accepts-invalid bugs according to the language reference / TSPL, which says: "All tuple types contain two or more types, except for Void which is a type alias for the empty tuple type, ().":

// This program compiles with Xcode 9.4 and Xcode 10 beta:
let t = (label: 12).label + (label: 34).label 
print(t) // 46

// This program compiles only with Xcode 10 beta and recent snapshots:
let t = (label : 123)
// We've just defined a single element tuple, and we can use it as such:
print(t.label) // prints: 123
switch t {
  case (label: 123): print("It's 123") // <-- Will match this case
  case let (label: v): print(v)
}
let (label: v) = t
print(v) // prints: 123

and they are also accepts-invalid bugs according to this error:

let b: (label: Int) = (label: 123) // Error: Cannot create a single-element tuple with an element label

So, as I've said before, until we have some common understanding and official declaration of the intentional design, reporting and fixing(?) such bugs(?) will probably only create more confusion.


This is the crux of the problem imo, and it's a pity if we really can't do anything about it.

Because then we have to live with serious problems (inconsistencies, confusion, limited expressiveness) caused by something as mundane as the choice of notation / which characters to use to indicate tuple elements.

I really don't see the issue with variadic generics, in all honesty, maybe I just don't understand what the point @Torust was saying. We don't know what the specific design of variadics in Swift will be. But from my understanding, the concern has to do with the return type being a tuple for everything except when there's only one element then it's not a tuple. But again I don't see why this is bad, maybe I'll need to use it in practice to see the issues it will cause.

enum with associated values, pattern matching are separate and were handled with SE-0155, which is not fully implemented, hence the new thread dealing with source compatibility issues regarding the full implementation of the proposal in today's Swift.

Box isn't generic. I'm simply pointing out that you can instantiate an object and get its property the same way you would for a tuple. Which I'm assuming was the point you were showing?

I realise now it's misleading to use Box due to it being a bit of a term of art. A physical cardboard box that has a sticker label was my thinking when writing this example :)

Right, so we've both been speaking with different assumptions. My assumption was that the documentation has a bug and yours is the compiler has the bugs. My thinking was that this is just a limitation of not being able to represent single element tuples and not a design decision. Hence to support it one would not need a proposal and the feature can just be introduced, though I may be wrong in thinking this.

Well, it would be source breaking, so you'd have to show active harm. I do see the argument for confusion, but not so much harm.

I think that, in order to have a meaningful discussion about tuples, we first have to know whether it is 1 or 2 that is the case in the following.


Assuming the current situation, that ((T)) == (T) == T, then either:

  1. All types are tuple types (1-tuples)

or

  1. There exist no such thing as a 1-tuple.

As far as I understand, these are the only two possibilities (please correct me if I'm wrong).


The difference between 1 and 2 might perhaps seem philosophical and without practical consequences, but here's an example of how it is not:

It doesn't seem unlikely that structural types (like tuple types) will one day allow extensions and protocol conformances.

So, once we can extend tuple types, something like this:

extension Tuple {
    func foo() { ... }
}

then, depending on whether 1 or 2 is the case:

  1. All types will have foo().

or

  1. Only (), (T, U), (T, U, V), ... will have foo().

I don't know which of these two makes most sense, but I whish the situation wasn't such that we must logically rule out a third (imo more intuitive) option, ie:

  1. Only (), (T), (T, U), (T, U, V), ... will have foo().

(But as long as ((T)) == (T) == T, case 3 is the same as case 1.)

2 Likes

This is expressly disallowed from the generics manifesto/Extensions of structural types. To quote the specific part:

There are some natural bounds here: one would need to have actual structural types. One would not be able  to extend every type:

extension<T> T { // error: neither a structural nor a nominal type
}

I'm not sure how 2 would work with the extension you defined. Could you rephrase using the notation from the manifesto?

Unless the Generics Manifesto disallows a protocol to which all tuple types conform, you can imagine Tuple in my example being that protocol. (Does the manifesto disallow such a protocol?)

Here is an attempt to rephrase my code example (without any such protocol and) using the notation from the manifesto:

extension<...Elements> (Elements...) {   // extending the tuple type "(Elements...)"
    func foo() { ... }
}

I'm not sure what the manifesto says about that, but my guess is that it's considered to be equivalent to:

extension<T> T { // error: neither a structural nor a nominal type
}

But is it really?

Anyhow, so far this points towards case 1 being the case.

Furthermore, the Generics Manifesto seems to be confident that the number of tuple elements is zero or more (not zero or two or more).

As can be clearly seen in this example from the manifesto.
public struct ZipIterator<... Iterators : IteratorProtocol> : Iterator {  // zero or more type parameters, each of which conforms to IteratorProtocol
  public typealias Element = (Iterators.Element...)                       // a tuple containing the element types of each iterator in Iterators

  var (...iterators): (Iterators...)    // zero or more stored properties, one for each type in Iterators
  var reachedEnd = false

  public mutating func next() -> Element? {
    if reachedEnd { return nil }

    guard let values = (iterators.next()...) {   // call "next" on each of the iterators, put the results into a tuple named "values"
      reachedEnd = true
      return nil
    }

    return values
  }
}

So I guess this means that the answer to my question is that:

  • Case 1 is the case, ie: All types T are tuple types, since T == (T). Except eg unconstrained generic type parameters, which are "neither structural nor nominal types". (Which makes me wonder if there are any other types that are not tuple types?) Perhaps a more correct formulation could be:
    "All nominal and structural types T are tuple types, since T == (T)"? (And this seems to imply that all nominal types are also structural, since tuple types are structural ... Could someone who knows this stuff please explain, @Douglas_Gregor ?)

So according to my interpretation of the manifesto, the language reference is wrong when it says:

A tuple type is a comma-separated list of types, enclosed in parentheses.

This is not true, because eg Int, which is the same as (Int), is also a tuple type, as is eg [Float?], none of which is a comma-separated list of types. And it's also wrong when it says:

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

This is not true, because a tuple type can contain zero or more elements.

Even the grammar is wrong:

GRAMMAR OF A TUPLE TYPE

tuple-type              → ( ) | ( tuple-type-element , tuple-type-element-list )
tuple-type-element-list → tuple-type-element | tuple-type-element , tuple-type-element-list
tuple-type-element      → element-name type-annotation | type
element-name            → identifier

How can The Language Reference and The Generics Manifesto be in such fundamental disagreement?

A week later.
I'm getting slightly worried that there is no common understanding (even among compiler devs) of Swift's tuple types. So I'll try again to get a definitive answer on at least this question:

Should these two programs compile or not?

Program 1:

let a = (label: 123)
print(a.label + a.label)

Program 2:

let a: (label: Int) = (label: 123)
print(a.label + a.label)

(Clearly, both programs should either compile or fail to compile. With Xcode 9.4, both fail to compile, but with the default toolchains of Xcode 10 beta 1 and 2, as well as with recent dev snapshots (eg 2018-06-24), Program 1 compiles.)

3 Likes

I asked about this on Twitter and it sounds like neither of those programs should compile. As I understand it, single-element tuples are currently only allowed due to implementation quirks and should be banned altogether.

When this is eventually (hopefully) sorted out at the implementation level, I'd be in favour of a proposal to introduce single-element tuples with some kind of special identifier syntax. At this stage, though, such a proposal sounds like it would be a recipe for bugs.

1 Like

Thanks! OK, though I'm still confused, I've filed:
SR-8109 - Swift 4.2 Regression: Single-element tuple not prevented

I agree, but I would prefer if the syntax for tuples (in general, not just single-element tuples) could be entirely separated from other concepts in the language that happens to be written using parentheses.

And I still welcome any clarifications to the questions raised upthread.

1 Like

Regarding this thread, as well as Jens' July 2016 posts, about parentheses, tuples, etc.: Jens' patience and enthusiasm for this topic are remarkable, and his writing on the subject is excellent. Thank you, Jens. Also, kudos to Letan and Thomas for their efforts and sharp counterpoints. Great discussion, all around.

These issues are a big deal. No matter their outcome, they have real implications for the long-run development of the Swift language.

I would be glad to see (or, if need be, help to create) a top-down decision tree for how parentheses are parsed and type checked by the compiler. I suspect that the parser and type checker are missing subtle opportunities to infer the intent of parentheses from context. I also suspect that there are edge cases where inference is impossible. Airing that sort of document would show us the specific problems, inform the discussion, and also might lead to creative solutions from the community.

The compiler has been built and modified over a long period of time by a lot of people. My impression is that, out of practical necessity, many modifications have been made as local patches rather than systemic integrations. At this stage, any real change to how parentheses are handled probably involves a frightening amount of retooling within the compiler. The work and the attendant risks of error are daunting, but waiting has its own costs. Whatever happens, great respect is due to the core team; they do amazing work.

Please see the commentary on SR-8089. In particular, there may be some significant under-the-hood work on tuples in the near future. My supposition is that that work may clear the way for better discussions about above-the-hood aspects of the issues addressed by this thread.

Let's find a way to keep this thread near the top of the heap.

5 Likes

To circle back on this: single-element labeled tuples are now banned: https://github.com/apple/swift/pull/21343

Note that in SR-8089, you're observing "parenthesis types", which are just sugar. (T) is equivalent T and does not designate a tuple type.

5 Likes

Ah, I see that it fixed https://bugs.swift.org/browse/SR-8109 now, nice.

I'd like to echo a question I wrote in a comment to SR-8172 here:

Are there any situations where a user would benefit from being able to observe a "parenthesis type"? Isn't it always just a pointless and confusing exposure/leakage of compiler-implementation details?

Also, why should Swift even allow us to write:
let x: (Int) = 123
in the absence of single element tuples?

To me, the above makes as little sense as:
(let) x: Int = 123

I use parentheses when the return type is a function because it helps me to read the thing: (T) -> ((T) -> U). Swift should definitely allow me to write that.

If arbitrary many other keywords could be used before and after let, and if surrounding them with parentheses in certain situations were required, then yes I would also expect Swift to allow me to write non-required parentheses around let as well.

4 Likes

(() -> ()) -> () and () -> (() -> ()) are different types. Similarly () -> ().Type and (() -> ()).Type are different.

I disagree that it’s an implementation detail - quite the opposite, keeping track of parentheses actually complicated things somewhat and if it weren’t for being able to print them back out we would have no reason to track them.

1 Like

Swift (and Clang and now GCC) have a separation between "types" and "canonical types" [1]. Types retain all their "sugar" including typedefs/typealiases/etc, but canonical types represent the semantic type which have all the sugar removed. Types are propagated and meticulously maintained by the compiler in an effort to improve diagnostics and make source translation tools work better, whereas canonical types are uniqued and pointer comparable / hashable.

We decided quite a long time ago that parenthesized types should be the same as the underlying type, which makes parens "sugar" just like typealiases are. I can't imagine changing this, as it would cause significant ripples throughout the type system, including source and ABI breakage.

-Chris

[1] The idea of canonical types originated (AFAIK) from a post by @Douglas_Gregor on the GCC mailing list in the ~2006 timeframe when he was grappling with variadic templates and other C++ features. I can't find the original post, but it really is a great idea, which is why Clang got built around it and Swift followed in those footsteps.

7 Likes

Is there anyway to demonstrate this in (Swift) code?

Terms of Service

Privacy Policy

Cookie Policy