Hi there! This is my first pitch so any feedback on the content, structure or presentation are very much welcome. I couldn't find any existing pitches on this topic though I'm sure it's been discussed. If there have been any material discussions on this topic feel free to point me to those threads. Thanks!
Introduction
Destructuring assignment is a language feature that allows you to extract multiple parts of a value and assign them to multiple variables within a single assignment statement. Swift already supports this feature for destructuring a tuple into multiple variables:
let (make, model, year) = ("Subaru", "Outback", 2017)
Which is equivalent to the following:
let car = ("Subaru", "Outback", 2017)
let make = car.0
let model = car.1
let year = car.2
The introduction of new variables is helpful as it gives more meaning to the values car.0
and car.1
which can be easy to mix up. You can see how destructuring syntax makes this pattern more ergonomic. While this is nice for tuples, destructuring is not currently supported for structs or classes which are very commonly used by Swift programmers.
The Pitch
I'd like to propose adding destructuring assignment for both structs and classes which would look something like this:
struct Car {
let make: String
let model: String
let year: Int
}
let car = Car(make: "Subaru", model: "Outback", year: 2017)
let { make, model, year } = car
The last line would be desugared to the following:
let make = car.make
let model = car.model
let year = car.year
The benefit here is slightly different than tuples as car.model
is already well-named at the point of use, but if used frequently the car.
part can become a bit noisy, especially with longer variable names. Pulling properties out into local variables is a fairly common way to get around this (in my experience) but requires redundant typing of the property/variable name:
let model = car.model
Destructuring allows the programmer to express this intent without the redundant keystrokes, and the savings scale linearly with the number of properties * the length of their names. This encourages the use of more descriptive variable / property names as well as extracting properties into local variables when used frequently. My opinion is that this is a net positive for both writing and reading Swift code.
There are a few key differences between destructuring structs vs tuples I'd like to point out. Destructuring a tuple is index-based and order matters, so the first variable declared on the left is assigned to tuple.0 the second variable on the left is assigned to tuple.1, and so forth. The destructuring of a struct/class is based on the names of the properties and order does not matter. This means the variable names must match the property names exactly. So the following line would still be valid in the example above:
let { year, model, make } = car
How ever the following would not compile:
let { manufacturer, model } = car //Error: Value of type 'Car' has no member 'manufacturer'
Additionally, I'm using curly brackets to wrap the variable names here (as opposed to parens for tuple destructuring) to notify the programmer that there are different rules involved. Structs/classes are opened/closed with {...} whereas tuple types are wrapped with (...), so I'm trying to reuse those visual indicators.
You do not have to extract every property in the assignment, you can extract whatever subset of values you'd like:
let { model, year } = car
Deeply Nested Values
Destructuring can be used to extract deeply nested values as well. Consider the following example:
struct Computer {
let name: String
let cpu: CPU
}
struct CPU {
let speed: Double
let numberOfCores: Int
}
let computer = Computer(
name: "MacBook",
cpu: CPU(speed: 2.8, numberOfCores: 8)
)
let { name, cpu: { numberOfCores } } = computer
// name: String
// numberOfCores: Int
// cpu is not introduced as a new variable, it is merely used to gain access to numberOfCores
Any time a nested value (like the CPU
here) is being destructured, a new set of curly brackets is introduced for clarity.
Assigning Methods
Destructuring should work just as well for methods as it does for properties. The destructuring assignment is always desuraged to a normal assignment with dot notation. So if the Car
struct had a method func start() { }
method, this would also work:
let { start } = car
// start: () -> ()
This would breakdown if start
were generic, you'd need a way to annotate the variable on the left to provide the type, so we might simply disallow this. Discussion on this scenario is welcome, though I imagine that destructuring against generic functions would not be a common use case.
var vs let Declarations
Destructuring can be used with both var
and let
declarations. Whichever annotation is chosen is applied to all variables in the assignment.
var { make, model } = car
is the same as
var make = car.make
var model = car.model
Dynamic Member Lookup
If a struct or class is declared with @dynamicMemberLookup
, destructuring assignment can be used against it with any arbitrary variable / property name. As a general rule, if the desugured code is valid, then the destructuring syntax is also valid.
Prior Art
Swift already supports destructuring for tuple values, so it already has a foothold in the language design. Because nominal types are used very commonly in Swift, it could be a nice addition to extend this feature to support them as well.
Destructuring assignment of object values has become incredibly popular in the JavaScript and TypeScript communities. While Swift and JS/TS are very different languages, they are not so dissimilar in the syntax for declaring variables and accessing properties, so some motivations may overlap in this area.
Not Included In This Pitch
This pitch solely covers property-based destructuring of structs and classes in assignment statements. It does not include destructuring of nominal types in function / closure parameter positions (like tuples in Swift and objects in JS), though this could be considered in a future pitch.
This pitch does not include index-based destructuring of Array values which is commonly found in other languages like JS and Python. In Swift, such a feature could trap on an invalid index so it would need to be handled with care.
Implementation
This feature could be implemented as an AST transformation in the compiler frontend. I don't yet have any experience contributing to the Swift compiler, so I don't have any more specifics at this time. If this feature is accepted, I would love to take a stab at implementing it with some guidance.
Effects on ABI Stability
This is purely a source level change so would not introduce any breaking changes to the ABI.
Effects on Source Stability
This feature is an additive change, so should not introduce any break any existing source files.
Effects on Compiler Performance
While {...}
syntax overlaps with other scenarios (e.g. defining a closure), it's placement to the left of an assignment operator should preclude any ambiguity and minimize effects on compiler performance.
Alternatives Considered
The primary alternative to adding this feature would be not to add this feature. Maybe everyone is happy with the current state of things, and that's ok.
Thank you for reading this pitch! I look forward to reading your comments.
[Edit] fix some typos
[Edit] add sections for var vs let and compiler performance