Roadmap for and state of single element tuple types?


(Jens Persson) #1

I have probably missed out on some information, and would appreciate pointers to such, regarding the following.


Some background from another thread

My conclusion from reading that entire thread, is that the preferred way to untangle the current tuple-type-related mess would be to …

… and I'm very much in favor of this solution, even though it would …


The default toolchain of Xcode 10 beta and versions up to the most recent dev snapshot (2018-06-14), has some new behavior that seems to suggest that work is in progress towards the above mentioned solution.

These are some examples of the new behavior that can be observed:

While the following does not compile (again, note that I'm only talking about Xcode 10 beta and later):

let x: (label: Int) = (label: 123) // Errors:
// 1. Cannot convert value of type '(label: Int)' to specified type 'Int'
// 2. Cannot create a single-element tuple with an element label

The following does:

let x = (label: 123)

And: The compiler accepts x as being a single-element labeled tuple value.

It won't allow explicitly declaring the type (as above), nor eg printing its type or dumping it …

print(type(of: x)) // Error: Expression type '(_) -> _' is ambiguous
                   //        without more context

dump(x) // Error: Type of expression is ambiguous without more context

… but it is demonstrably a single element labeled tuple, since eg:

1.
Option clicking x will show the declaration:

let x: (label: Int)

2.
The list of code completions for x. is:

                  Int label
         (label: Int) self

3.
If we try to access x as being implicitly cast to Int, we will get a compile time error mentioning the tuple type:

let y: Int = x // ERROR: Cannot convert value of type '(label: Int)' to
               //        specified type 'Int'

4.
We can, and have to, use the label in order to access its single element:

let y: Int = x.label

Without knowing what the intended behavior is, I can't be sure what is and isn't a bug. Ie, if there is a work in progress towards the above mentioned solution, then the following are examples of bugs:

let x: (label: Int) = (label: 123)
// Actual Result: Two invalid errors:
//   1. Cannot convert value of type '(label: Int)' to specified type 'Int'
//   2. Cannot create a single-element tuple with an element label
// Expected result: This should compile successfully

print(type(of: x))
// Actual Result: Invalid error:
//   Expression type '(_) -> _' is ambiguous without more context
// Expected result: This should compile successfully and print: (label: Int)

dump(x)
// Actual Result: Invalid error:
//   Error: Type of expression is ambiguous without more context
// Expected Result: This should compile successfully and dump x.

But if this is not a work in progress, and (T) is expected to be implicitly cast to T, then the examples in this post demonstrate a totally different set of bugs.


Confusing implicit conversion of enum case associated values to typle
Same-named argument and tuple labels?
Pitch: Deprecate strange interpolations in Swift 4.2
(Tino) #2

My impression is that tuples don't get enough attention:
There's much interest in large topics (existential types, variadic generics...), but tuples don't seem to be important enough to write manifests for them - yet issues regarding them pop up all the time (the latest probably Some small keypath extensions: identity and tuple components).

I think a long-term plan for tuples and the significance of labels (not only in tuples) could help a lot.
Your milage may vary, but I had to fire up the compiler to check what is possible and what not, and I have no idea how Swift 7 might deal with some tuple assignments...

var tup0: (Int, Int) = (key: 2, 2)
let tub0: (a: Int, b: Int) = (2, b: 1)
tup0 = tub0 // works

var tup1 = (key: 2, 2)
let tub1: (a: Int, b: Int) = (2, b: 1)
tup1 = tub1 // doesn't compile

var tup1b = (key: 2, 2)
let tub1b: (a: Int, b: Int) = (2, b: 1)
tup1b = tub1b as! (key: Int, Int) // compiles & crashes
	
var tup2: (key: Int, Int) = (2, b: 1)
let tub2 = (key: 2, second: 2)
tup2 = tub2 // works

var tup3: (key: Int, Int) = (2, b: 1)
let tub3 = (first: 2, second: 2)
tup3 = tub3 // doesn't compile

(Jens Persson) #3

I think so too (assuming there isn't already one).

And I think an essential part of such a plan would be to drop as much special casing as possible, including the current ban, and any subsequent hypothetical implicit casting of single-element tuples.

We now have

0,    2, 3, 4 ... -tuples

And the only reason I can see why we can't have

0, 1, 2, 3, 4 ... -tuples

has to do with the unfortunate fact that parentheses are so overloaded. Here are some different things they are used for:

  1. Tuples
  2. Parameter- and argument lists
  3. Pattern matching
  4. enum associated values
  5. "grouping stuff" like (a + b) * c and let a: (((Int))) = 123.

And more specifically, referring to this imperfect list and accepting that parentheses has to be used for all these things, I think it's the extreme permissiveness of 5, or the tendency to conflate 5 with the other ones in all sorts of contexts, that make people unsure about whether they think

let foo: ((Int)) = 123

should mean that the type of foo is

  • Int

or

  • ((Int)), that is "a 1-tuple of a 1-tuple of an Int", just like [[Int]] is "an array of an array of Int".

I cannot see how the special case of implicitly casting (Int) and ((Int)) etc to Int in this context makes any sense at all. It can only be motivated by conflating two very different concepts, which are unfortunately both expressed using parentheses.


Then what about for example:

let bar = (123)

I could try to argue that bar should be 1-tuple here, since the parentheses should not be there if the intention was that bar should be inferred to be Int, but I would probably lose.

Also, I wouldn't want to introduce the following error:

let baz = (1234 + 567) * 8 // ERROR: Binary operator '*' cannot be applied to
                           //        operands of type '(Int)' and 'Int'

Let's think about how much simpler these things would be if we didn't have to overload (…), […], {…} or <…>. If tuples had been written using eg […] instead of (…), my guess is that no one would support an idea like "[Int] should be equivalent to Int since superfluous brackets are stripped" (but using [ ] for tuples would of course only make us conflate tuples and arrays instead of tuples and "generally-grouping-stuff"-parentheses).


My opinion is that we should not ban or otherwise special case 1-tuples, because it is complicating our mental model of the system and, I guess, its implementation.

Instead, we should find the best* way to syntactically disambiguate conflicting uses of parentheses.

(*) "best" as in simplest (no special cases), easiest to remember, read, write and understand, etc.


(Thomas Roughton) #4

I'm sure this has been suggested before, but how about a Python-like (element,) syntax for one-tuples – i.e. requiring a trailing comma after the first element? That means it's easy to disambiguate and the syntax is reasonably consistent.

e.g.

let x = (label: 123, ) // single-element tuple of type (label: Int)
let y = (34, ) // single-element tuple of type (Int)
let z = (34) // Int


(Jens Persson) #5

How would the the types be written?

print(type(of: x)) //   (label: T)    or   (label: T, )
print(type(of: y)) //          (T)    or          (T, )

I guess the latter? And ((Int)), would be ((Int ,), ),
and (Int, (Int)) would be (Int, (Int, )).

It's a good thing that the same syntax is already allowed (but entirely optional) in eg array literals:

let a1 = [3, ] // You don't have to do this in order to prevent [3] from being
               // implicitly cast to an Int. But it's accepted syntax.
let a2 = [2, 1, ]

But I would prefer a solution that more clearly separated tuple delimiters from general-grouping-stuff-parens, so it should not be something that applies just in the single-element case. Perhaps something like requiring a dot (signaling element-accessability) after any parentheses intended as tuple-delimiting:

let tupleA = (1, ((2 + 3) * 4).).  // type of tupleA is written (Int, (Int).).
let tupleB = (123).

#6

Indeed, a few years ago we had a somewhat related pitch [Draft] Allow trailing commas in argument lists Some of the discussion went here, when we moved to the forum: Allowing trailing commas in argument lists

I think, somehow, that would nicely fit together with the tuple of one problem.


(Thomas Roughton) #7

My personal preference would be the types exclude the commas to match with higher-order tuples. I’d also suggest extending the tuple syntax so that a trailing comma is valid for any tuple, but is only required for the 1-tuple.

To me, the . is a little less clear since it reads as a member access; when typing that in Xcode you’d just end up with code completion suggestions which can be hard to dismiss.

With that said, the exact syntax can be saved for a discussion thread (or perhaps this will become one) if the consensus is that special syntax is needed to support 1-tuples; I’m not tied to any of this, but I do think special-case syntax is the right approach.


(Jens Persson) #8

I'd like to widen it a bit, into something like "if special syntax is needed to prevent further conflation of entirely different concepts that happen to be written using parens, eg conflating tuples, argument lists and generally-grouping-stuff-parentheses."


(Letanyan Arumugam) #9

I don't think that reads well to someone just coming in without knowing this exists (It just looks like an error to me).

I think we should ask ourselves what is the use case for a single tuple is. When I do this the only use case I can see is to label it, hence we only need to support single tuple if it has a label, else treat it like a normal type.

let a = (label: 10)
let b: (Int) = (10)

type(of: a) // (label: Int)
type(of: b) // Int

(Jens Persson) #10

If/when Swift gets fixed size arrays, and if they have a type level element count, what is the use case for a single element fixed size array?

I think it's just asking for trouble (the trouble we are currently in) to design a system with weird exceptions like banning single element tuples (leaving a gap between zero- and two-element tuples), instead of striving for consistency and simplicity. If tuples were written using eg ⟪ ⟫ we would not have this discussion, and no one would get the idea that ⟪⟪⟪T⟫⟫⟫ should be equivalent to the entirely different type T.

Not all pieces of a system need to be motivated by a use case, they might instead come naturally and be motivated by the cost (in confusion and complexity) of breaking consistency in order to exclude them.


(Thomas Roughton) #11

Most of the time I'd only want to use a labelled 1-tuple (I've often wished I could use one as a return value from a method). However, I think once variadic generics are available in the future, the use case gets stronger, and not including 1-tuples requires implicit splats by the compiler.

For example, I've got a series of methods in one codebase with the signature:

func reallocate<A>(capacity: Int) -> (UnsafeMutablePointer<A>) 
func reallocate<A, B>(capacity: Int) -> (UnsafeMutablePointer<A>, UnsafeMutablePointer<B>)
func reallocate<A, B, C>(capacity: Int) -> (UnsafeMutablePointer<A>, UnsafeMutablePointer<B>, UnsafeMutablePointer<C>)

These are currently auto-generated from a template. Now, if variadic generics were available, this would become one method. Given that, it makes more sense to me that the following should match:

var a: UnsafeMutablePointer<Int>
var b: UnsafeMutablePointer<Float>
 
(a) = allocator.reallocate(capacity: 10) 
(a, b) = allocator.reallocate(capacity: 10)

and that the return value of the one-tuple-returning method shouldn't get implicitly converted to its value; i.e. that the syntax shouldn't be:

a = allocator.reallocate(capacity: 10) // should be invalid, but currently is accepted.

since the return type is a tuple, not its contents. Of course, I don't know what removing that implicit splat would do to source compatibility and I'm guessing the breakage would be fairly significant. Still, it's an argument for a no-label 1-tuple.


The Generics Manifesto and single-element tuples
(Letanyan Arumugam) #12

Semantically arrays are different than tuples. Tuple's are what models most of our type system and the flattening is for convenience to remove behaviour that would otherwise be an irritation to work with. Unlike arrays where one would be surprised if it were flattened.

I don't think single tuples are banned they're just sugared from (T) into T and vice versa. The same way (5) == 5.

I guess my mental model, as mentioned above, alleviates concerns I would've had for this behaviour.


To clarify my position I'm not against having a single element tuple. Just as long as it doesn't change any current behaviour and doesn't add more syntactic weight. Someway of building a single element from current language syntax like Tuple<T> would be ideal IMHO.

func f() -> Tuple<Int> {...}
let a: Tuple = f() // (Int)

let a = (Tuple<Int>(5), 2) // ((Int), Int)

(Thomas Roughton) #13

I don't think single tuples are banned they're just sugared from (T) into T and vice versa. The same way (5) == 5.

Just to pick up on one small point: under the proposed model, (5) would not be a tuple; you'd need to explicitly say (5, ) (or (5)., or Tuple<Int>(5), whatever is decided upon).


(Jens Persson) #14

(I wrote fixed size arrays, and edited to add type level count, in order to make the analogy more relevant, but let's go with regular arrays.)

Sorry for repeating this but it's so easy to forget: Parentheses are unfortunately used for several entirely different purposes, two of which are:

1. To group parts of an algebraic expression, as in (x + 2) * y. Here, they are used to induce order of operations by overriding normal operator precedence. It's easy to see how and why parentheses when used in this sense can be superfluous: The order of one or zero things (operations) always stays the same, so the following parens have no effect: (1 + (2)). Parentheses (when used in this sense) can also be superfluous in that the precedence of the (2 or more) operators are such that the order isn't changed by the parentheses, as in eg: 1 + (2 * 3). The convention is to allow such superfluous parentheses. They don't change anything, and can be used to increase clarity (for human readers).

2. To identify tuple elements, as in
let tuple: (UInt8, (String, Float)) = (1, ("two", .pi)). Here parentheses are used to identify the elements of a tuple type or literal, just like square brackets are used to identify the elements of an array. But tuple types can have a different type for each element, and the elements can have labels, and the number of elements are at the type level.

Oh, and tuples can have 0 or 2 or more elements, not 0 or more like arrays.
– But why can't they have one element?
– Well, ...

... because ...

... in Swift's early days we thought it would be possible to use tuples (including single element ones) for almost everything, but then we realized that all aspects of parameter lists (inout, @noescape etc) cannot be represented as a tuple, and since we use parentheses for a lot of things, we sometimes conflate these things, and it gets confusing, and it's kind of hard to communicate with all these different things dressed up as parentheses, but at least we solved the parameter list / function type issue by always requiring explicit parameter-list-parens but ... long story short: We've had and still have a lot of confusing discussions, bugs and inconsistencies related to tuples, argument lists, pattern matching, almost anything having to do with parentheses, and banning single element tuples seemed to solve at least some of the problems, and it's also for convenience, at least in some cases, even though it's not so great for consistency, and it does imply some other weird inconsistencies and needless limitations (for eg variadic generics). Also ((1) + (((2) * 3))) is the same as 1 + 2 * 3 so why shouldn't ((Int)) be the same as Int? After all, A product of types is a direct product of two or more types, so we could have used an infix * instead of parens to indicate tuples, and Haskell has no 1-tuples, at least not in their parenthesized tuple syntax.


Another big difference between the semantics of tuples and eg arrays:

I've had no trouble understanding or using Swift's arrays, their behavior seems to allow me to easily form a working mental model, the rules are few and consistent, no strange exceptions, very few things to keep in my head, I've never been surprised or frustrated by them, or worried about their design, implementation and effect on other parts of the language.

But with tuples, it's been quite the opposite for as long as I can remember. My mental model of them are a tangled mess, which might be somewhat in line with their implementation in various versions and parts of the compiler. And what the language reference says about them doesn't make much sense to me and is demonstratably not true in a lot of cases.


I'm not sure I understand what you mean, could you please clarify?


Just as a thought experiment, assuming Swift's tuples were written using eg ⟪…⟫ instead of (…), and ⟪⟪Int⟫⟫, ⟪Int⟫ and Int were three different types, and only the first two tuple types. Would there still be such irritating behaviour, and if so, can you give an example?


I guess it is possible that Int is considered a no-label 1-tuple type, and I'd like to know if this is the case, and what practical/technical reasons and consequences it has. Here's what the Swift Book has to say about it:

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

And in Xcode 10 beta and later, we have this situation:

// we can instantiate and use labeled 1-tuples:
let a = (label: 123)
print(a.label + a.label) // 246

// but not if we specify the type explicitly:
let b: (label: Int) = (label: 123) // Error: Cannot create a single-element tuple with an element label

But it's the special casing of 1-tuples (no matter if they are "banned" or otherwise special) that I think is problematic. Special casing spreads as in @Torust's example.


It seems to me like you're just accepting this loss in expressivity and uniformity that comes from special casing 1-tuples (which in turn afaics might not even have been deemed necessary if tuples hadn't been written using parentheses).


I think it would be great if we could find some way of not special casing 1-tuples, syntactically or otherwise, and which also made all tuple expressions (not just 1-tuples) clearly distinct from parenthesized expressions, just to get rid of all potential confusion.

But if Int has to be a tuple type for some reason, then I don't know, and I'd like to know if Int is both a basic primitive type and a compound type?

Regarding not changing current behavior, here are some examples of the current behavior:


// 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 with Xcode 9.4 and Xcode 10 beta:
let t = (x: "first", x: "second")
let (a, x: b) = t
print(a, b) // prints: second first

// 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

// This program compiles with Xcode 9.4 and Xcode 10 beta:
let `true` = !(true: true).true // Drop .true to crash compiler!
print(`true` == false) // prints true

(Tino) #15

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

#16

What about:

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

And maybe

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

?


(James Froggatt) #17

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.


Some small keypath extensions: identity and tuple components
(Letanyan Arumugam) #18

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.


(Jens Persson) #19

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.


(Letanyan Arumugam) #20

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.