For my part, that’s not really what I’m looking for. I’m most interested in refutable patterns in places where we use the case
keyword. For this purpose, it’s important that the syntax include a subpattern that can either match or bind the property’s value. I think irrefutable patterns would also fall out of any reasonable design for the feature, and I think they’ll be very useful, but it’s not my primary focus.
So, I see the motivation for this pitch, though I don't know if any of the solutions proposed are actually worth the hassle.
The initial pitch has a simple and clean syntax (minus the curly brackets, which as other have mentioned are a no-go), but it has the problem of only letting you destructure a single instance of an entity in a scope.
The subsequent pitches allow for various things like different variable names, weak variables, nested destructuring, etc but have syntax that is, frankly, unwieldy and a little convoluted. You end up with something that is harder to read, harder to understand, and as a result harder to learn than the supposed "boilerplate" it is seeking to replace.
A lot of this does feel a bit like wanting code to be clever and terse over wanting code to be clean and readable. There is nothing really all that wrong with the below unless you deeply care about character count or line count (which you should never do)
let make = car.make
let model = car.model
let year = car.year
The above code is clear, it is simple, it is easy for both a novice and an expert to read and write. And on top of that it is fewer characters than some of the proposed solutions while being more flexible than the others. In a way it is already the best solution to the problem of destructuring an entity into variables.
I think we also need to think about how common the above code is. How often do we need to extract a large number of properties from a struct into local read only variables within a single scope? It's not often. Occasionally you may need quite a few vars
but you're more likely to just modify the class or struct directly. The most likely place to need something like that is in guard let
/if let
statements, but it's rare you're doing that for multiple properties on a single entity.
In many ways the above code is more of a code smell, and it feels a bit like we're asking what perfume to use to disguise it rather than just eliminating the smell (breaking up the method or just foregoing the local variables in the first place). Then again it could just be my usual tendency for "Swift has too many features/too much syntax and should have a ridiculously high bar for new ones"
wholeheartedly agree with this sentiment and the rest of what you've just said.
Within Swift, there is a much smaller and cleaner language struggling to get out!
Hi there, OP chiming in here. Thanks for all of the feedback and after reading your comments I no longer think this feature would be a good addition and won't be providing any more input (explanations below). However it was really fun and inspiring to post my first pitch and engage with all of you here, thanks for spitballing with me and I hope to provide more valuable pitches in the future.
While my motivation was not to make Swift like JS, I think my experience with JS heavily drove the motivation. I think there are things in the JS/TS that make this feature valuable that don't carry over to Swift. Just one example, historically React components have had a single state
object and a single props
object from which you must extract multiple values to render whereas in a SwiftUI view you can simply have multiple @State properties and multiple let
properties for "props". The values are already split apart. The problems I've presented are really not that problematic or common in Swift, so this feature does not meet the high bar required for new syntax IMO.
The discussions on this thread quickly got out of scope from the original pitch (which was purely about assignment statements), specifically talking about switching on a struct
(or similar property based value). After thinking about it, I don't think this feature makes much sense either. switch
works very well for exhaustively handling a set of mutually exclusive cases (eg sum types), and product types / property based values don't really line up with this at all. I don't think we'd ever want to force exhaustivity for switching on / destructuring a struct because then adding a new property would be a breaking API change. As @wtedst mentioned in the first reply, we'd then have to start thinking about @frozen or @unknown for almost every type. I didn't quite understand their point at the time but with exhaustivity in mind it makes total sense.
If you'd like to to enumerate some property values, putting them in a tuple makes perfect sense. You've scoped what properties you'd like to handle, instead of having to think about the whole range of properties available, which can come from many different places outside the original type definition, eg extensions and protocol conformances with extensions.
switch (car.model, car.year) {
// very easy to exhaust these cases compared to any and everything that might exist on car
// in reality you probably want a very limited subset of the properties
}
Additionally, if-else
works just as well switch
ing on a struct value and with much less noise. Take the following example that was posted above:
Is this really that much better than:
if car.make == "Porsche" {
print("Nice car!")
else {
print("This car was made around \(2022 - car.year) years ago.")
}
It's less code overall and very easy to read. Rebinding the values to variables in the case statements provides very little value: being able to just write year
instead of car.year
is not a win given the amount of other keystrokes required to get there.
For all of those reasons I am now -1 on this pitch but please continue to iterate if you think there's something valuable lurking here.
Have a nice day!
-Pat
I honestly don't see any particular utility in the OP proposal. Being able to write something like this
let { make, model, year } = car
instead of
let make = car.make
let model = car.model
let year = car.year
doesn't seem, at least to me, to justify the surely substantial changes to the compiler, just for saving a couple of characters. AFAIK, Javascript "type system" is essentially structural, which is not a good match for Swift.
But the idea of destructuring structs for (exhaustive) patter matching purposes is one of the key missing features in Swift, in my opinion, and would be extremely useful, exactly for the same reasons why exhaustive pattern matching on enums is useful.
I would actually look a bit further ahead though: the actual missing feature in Swift, I think, is generalized pattern matching with variable binding. What I would actually like to write, one day, is something like:
let foo = "some interesting string"
switch foo {
case ("some ", let adjective, " string"):
print(adjective) // prints "interesting"
default:
break
}
struct Person {
var name: String
var age: Int
}
let bar = Person(name: "Foober", age: 42)
switch bar {
case (name: "Foober", age: let age):
print("Foober is", age) // prints "Foober is 42"
case (name: _, age: let age):
print("Someone is", age) // prints "Someone is 42"
}
This would be a 2-step process:
- define a syntax to declare a pattern with variable binding;
- synthesize patterns for common types and structures (possibly under a protocol conformance, like
Matchable
).
For example, in the Person
and String
cases, the syntax could be something like:
func ~=(pattern: (&name: String, &age: Int), value: Person) -> Bool {
name &= value.name
age &= value.age
return value.name == name && value.age == age
}
func ~=(pattern: (&portions: Substring...), value: String) -> Bool {
portions &= value
return value == portions.reduce("", +)
}
Although the proposal has basically been withdrawn now, I just wanted to chime in on this because I think I've seen this argument a couple of times. "Saving a couple of characters" was never the point. It's about eliminating redundancy, which is not about terseness or brevity for its own sake, but rather reducing opportunities for error.
I think for nominal types, you'd want to be able to delegate the destructuring to the type (in the same way that construction is delegated to the type). I'm thinking of something like Scala's unapply
function that's used for object extraction. That would also be useful for generalised parsing/regexes in the future.
I share what you said.
On the apple web site it is written:
"Swift is a robust and intuitive programming language...
Swift is easy to use..."
But nowadays it is already not so easy and intuitive for newcomers in many places. The problem we discuss is not so big in general, but solutions complicate the language. May be in some projects it is needed more often than in others. But those people can write about ten functions to achieve the goal, like this one:
func destr<T, A, B, C>(_ instance: T, _ body: (T) -> (A, B, C)) -> (A, B, C) { body(instance) }
let (model, year, brand) = destr(car) { ($0.model, $0.year, $0.brand) }
We can also write some other functions to make code shorter when we don't need destructuring:
func doWith<T>(_ instance: T, body: (T) -> Void) { body(instance) }
func mutate<T>(_ instance: inout T, body: (inout T) -> Void) { body(&instance) }
doWith(car) {
print($0.model)
print($0.brand)
}
mutate(&profile) {
$0.name = "personName"
$0.birthDate = "01.01.2021"
}
Almost always we don't need to destructure all properties of a class or struct, we need only several of them.
In my own experience the need of destructuring is not so often. In our codebase about 55k lines of code it can be used in less then 10 places.