Deprecating Tuple Shuffles (Round 2)

Hello all,

@hartbit and I have been thinking a lot about an esoteric feature of Swift called Tuple Shuffles. To that end, we're seeking its removal and believe that we have a proposal ready to do so which I have attached below.

Attentive watchers of the Swift Evolution of old will remember that I've raised this idea before. If you want to go see what a really early sketch of this proposal look like, that's over here. Now that we're aiming for a more stable Swift 5, this is as good a time as any to bring forward a rare feature-removal proposal for serious consideration.

The code to implement this proposal is trivial, but if you want to go play around with it all the same, it lives here

Deprecate Tuple Shuffles

Introduction

This proposal seeks to deprecate certain forms of a little-known feature of Swift called a "Tuple Shuffle"†. A Tuple Shuffle is an undocumented feature of Swift in which one can reference tuple labels out of order in certain expressions.

Tuple-Shuffle Examples

Tuple Shuffles are best seen in action:

  • Assigning to a tuple with the same labels in a different order:
let a = (x: 1, y: 2)
let b: (y: Int, x: Int)
b = a
print(b) // prints (y: 2, x: 1)
  • Destructuring a tuple and by referencing labels out of order:
let a = (x: 1, y: 2)
let (y: b, x: c) = a
print(b) // prints 2
print(c) // prints 1
  • Mapping parameter labels out of order in a call expression:
func foo(_ : (x: Int, y: Int)) {}
foo((y: 5, x: 10))

Non-Examples

Similar syntax that does not reorder tuple labels are not Tuple Shuffles:

  • An assignment to a tuple with the same labels in the same order:
let a = (x: 1, y: 2)
let b: (x: Int, y: Int)
b = a
  • An assignment to a tuple without labels:
let a = (x: 1, y: 2)
let b: (Int, Int)
b = a
  • Destructuring a tuple and by referencing labels in order:
let a = (x: 1, y: 2)
let (x: b, y: c) = a
  • Re-assignment through a tuple pattern:
var x = 5
var y = 10
var z = 15
(z, y, x) = (x, z, y)

Motivation

The inclusion of Tuple Shuffles in the language complicates every part of the compiler stack, contradicts the goals of earlier SE's (see SE-0060), and makes non-sensical behaviors possible in surprising places.

Consider the following:

var a: (Int, y: Int) = (2, 1)
var b: (y: Int, Int) = (1, 2)

a = b
print(a == b) // false!

This reveals an inconsistency between the language and its standard library (where equality of tuples is defined). Where the language permits the first assignment to succeed by virtue of an implicit Tuple Shuffle, the equality fails because the Swift Standard Library considers tuples equal when their elements are index-by-index equal. The rest of the Swift has seemingly agreed that operations on tuples preserve and respect their parallel structure. The rest of Swift, that is, except Tuple Shuffles.

This proposal seeks to deprecate Tuple Shuffles in Swift 4 compatibility mode and enforce that deprecation as a hard error in Swift 5 to facilitate their eventual removal from the language.

Proposed solution

Construction of Tuple Shuffles will become a warning in Swift 4 compatibility mode and will be a hard error in Swift 5.

Detailed design

†Throughout this proposal, we have referred to "Tuple Shuffles" as a monolithic feature. However, the compiler currently models many things with Tuple Shuffle Expressions including variadic and default arguments. For the purpose of this discussion a Tuple Shuffle is defined to be any Tuple Shuffle Expression that causes the labels in its type to be reordered.

All of the examples above fit this model:

let a = (x: 1, y: 2)
let b: (y: Int, x: Int)
b = a // Shuffles x -> y, y -> x
let a = (x: 1, y: 2)
let (y: b, x: c) = a // Shuffles x -> y, y -> x
func foo(_ : (x : Int, y : Int)) {}
foo((y: 5, x: 10)) // Shuffles x -> y, y -> x

The compiler shall continue to accept and construct all forms of Tuple Shuffle Expression under Swift 4 compatibility mode. In Swift 5, Tuple Shuffles will be removed from the language.

Impact on Existing Code

Because very little code is intentionally using Tuple Shuffles, impact on existing code will be negligible. In fact, turning on the error-producing behavior we intended for Swift 5 in all compiler modes passes the Swift Source Compatibility Suite.

Alternatives considered

Continue to keep the architecture in place to facilitate this feature.

30 Likes

It would shore up the “Impact on Existing Code” section if you tested your implementation on the compatibility suite. The pitch is very clear and I would have no objection removing this feature from the language, as long as the source compatibility impact is found to be small.

2 Likes

Doing that as we type.

2 Likes

From an implementation perspective I’m in favor of this proposal.

5 Likes

I didn't even know the language had this "feature". I can't agree more with its removal, from both user and implementation perspectives.

6 Likes

I concur with the other's sentiments with the removal of this 'feature' from the language. To be honest I didn't even know it existed.

I am also in favor of this from an implementation perspective. There is a ton of code in SILGen just to maintain this feature. Not worth the complexity IMO.

1 Like

I need to re-read the old discussion regarding the removal of tuple shuffles, because I miss the point why you're removing unordered destructuring from the language?!

It's not that uncommon that the community asks for some sort of syntax to easily extract values from types, which by the end of the day is nothing other then destructuring. If we had to generalize that feature, I'd say it should be like "extract any type property into a (labeled?) tuple". I might be wrong, but I don't see a reason why destructuring into a tuple should preserve order or the same amount of values as the type has elements/properties.

In this example taken from the proposal I don't see any shuffling, but rather destructuring by explicitly referencing the elements of a tuple by their labels:

let a = (x: 1, y: 2)
let (y: b, x: c) = a
print(b) // prints 2
print(c) // prints 1

To keep the consistency the destructuring part that is proposed to be removed should stay.

struct Point3D {
  var x: Int
  var y: Int
  var z: Int
}

// Partly destructured value into a tuple; Order cannot be preserved,
// especially if there are properties in extensions.
let (y: b, x: a) = Point3D(x: 0, y: 1, z: 2) // a == 0, b == 1 

Long story short: If I just forgot the motivation for the removal of that particular functionality, I'd really appreciate if you'd mention it in the proposal.

2 Likes

I personally think that if there's a destructuring feature that is going to work across all types, not just tuples, it should use a syntax more aligned implicit member expressions (e.g. .x, .y) or keypaths (e.g. \.x, \.y) on the left hand side, rather just implicitly matching tuple labels with instance variables.

Edit: And removing tuple shuffles would be a good first step to clear up this space for a feature which is better designed and doesn't have the current limitations. e.g. it would be nicer if your Point3D example would also work with an (x: Int, y: Int, z: Int) tuple using the same syntax, but the tuple shuffle requires the number of elements to match so you would need to mention z as well.

4 Likes

Can you explain that part more in detail. I'm not sure I can follow here. 'Future' value destructuring should support an arbitrary amount of destructured values from the value (in respect to the accessible elements/properties).

let point = Point3D(x: 0, y: 1, z: 2)

let (y: b, x: a) = point // a == 0, b == 1 

let (x: c, y: d, z: e) = point // also valid

let (x: f) = point // also valid even if not very useful

Or do you want destructuring into a tuple?

let tuple: (x: Int, y: Int, z: Int) = point // like this?

I mean that if you replace Point3D with a tuple, instead of a struct, the current tuple shuffle feature doesn't allow you to do most of those things, because it is strictly a shuffle:

let point = (x: 0, y: 1, z: 2)
let (y: b, x: a) = point // error: '(x: Int, y: Int, z: Int)' is not convertible to '(y: _, x: _)', tuples have a different number of elements
let (x: c, y: d, z: e) = point // valid due to tuple shuffle, can reorder labels
let (x: f) = point // “accidentally” valid but doesn't do what you want, f has type (x: Int, y: Int, z: Int)

So if there's going to be a destructuring syntax I would prefer it was just simple shorthand for accessing member variables from any type, and don't think it should be based on this unfortunate feature. e.g. something like this strawman syntax

let (.y: a, .x: b) = point
// equivalent to
let a = point.y
let b = point.x 

or maybe \.y etc like keypaths. Then it would apply and behave in the obvious way no matter what the type of point is (tuple, class, struct, enum…).

4 Likes

Okay I see now. If I had to vote, I'd choose the key-path like labeling because it's probably the a more powerful way to destructure a value since it can traverse the value even further and extract nested values.

I'm actually surprised that the labeled single element tuple exists in that form today (I used it as a hypothetical example). @Jens didn't you already discovered that and filed a bug report for labeled single element tuples at that particular location?

For a historical perspective, tuple shuffles are very old and were not well considered. They date back to a time where functions aspired to take a single value and produce a single value (which we're long past at this point, given that inout and several other things do only make sense in argument lists) and date back to when we were toying with the ability for keyword arguments to be reordered arbitrarily.

Both of these design decisions failed, and yet we still carry the cost of this old feature. I'm super super +1 on dropping it - it isn't paying for itself and it leads to incredible cost in the implementation that we'd be better off without.

-Chris

16 Likes

What I reported ( SR-8109 ) was the possibility to define a labeled single element tuple like this:

let singleElementTuple: (label: Int) = 123

but this is now an error in the default toolchain of Xcode 10.

The bug is still open, as it should, because although the following two variants are now errors:

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

let t: (label: Int) = (label: 123) // ERROR: Cannot convert value of type '(label: Int)' to specified type 'Int'
                                   // ERROR: Cannot create a single-element tuple with an element label

it's still possible to do this:

let t = (label: 123)
print(t.label + t.label) // prints 246 (which is inconsistent with the above errors)

Regarding these examples:

The lhs of those assignments are not declaring any tuples; a, b, c, d, e and f are just Int variables (constants).

I guess the lhs of the first two assignments are using value binding patterns to "decompose the elements of a tuple and bind the value of each element to a corresponding identifier pattern".

The lhs of the third assignment looks the same but I guess it's a tuple pattern of one element, which is ignored according to the documentation:

The parentheses around a tuple pattern that contains a single element have no effect. The pattern matches values of that single element’s type. For example, the following are equivalent:

let a = 2        // a: Int = 2
let (a) = 2      // a: Int = 2
let (a): Int = 2 // a: Int = 2

The documentation doesn't mention anything about a label being allowed in this case, one that can be anything, as that is also ignored. But according to the default toolchain of Xcode 10, it's ok to have a label, and it can be anything:

let point = (x: 0, y: 1, z: 2)
let (xxx: f) = point // Still compiles
let (and: (this: g)) = point // Still compiles

let a = (asDoesThis: (same: print(f), same: print(g)))
// The above line will print:
// (x: 0, y: 1, z: 2)
// (x: 0, y: 1, z: 2)
print((hello: (world: a))) // prints (same: (), same: ())

I'm very much in favor of simplifying the implementation and concept of tuples!

1 Like

+1 for consistency & simplification.

1 Like

The proposal mentions the following among the examples of things that are not tuple shuffles:

var x = 5
var y = 10
var z = 15
(z, y, x) = (x, z, y)

I'd like to suggest adding something like the following example, which as far as I understand is not using any tuple shuffles either (as they are destructuring a tuple into a tuple (the same even), right?):

var point = (x: 1, y: 2, z: 3)
print(point) // prints (x: 1, y: 2, z: 3)

point = (point.y, point.z, point.x)
print(point) // prints (x: 2, y: 3, z: 1)

point = (x: point.y, y: point.z, z: point.x)
print(point) // prints (x: 3, y: 1, z: 2)

If I'm correct in that this isn't using any tuple shuffles, then this example is good to include as it might look more similar to a tuple shuffle than those already in the proposal.

(But if I'm wrong, and it is using tuple shuffles, then I'd like to understand why/how.)

1 Like

Also, it might be worth taking same named argument and tuple labels into consideration within this context. Here is a puzzling example of them and tuple shuffling in action:

let a = (x: 1, x: 2, x: 3)
let b: (Int, x: Int, x: Int) = a
print(b) // prints (3, x: 1, x: 2)

+1 this definitely does not seem worth keeping around. It would be a huge code smell to see any of these examples used in a codebase. Let it burn!

+1 please.

Minor point, but the double negative here seems to be incorrect.