Record initialization and destructuring syntax

Great to hear there's some work done on this. Is any of this available open-source within SourceKit codebase or is this a proprietary Xcode logic?

It's all in the open source sourcekitd. E.g. complete after the comma in the call below:

func foo(a: Int, bbb: Int) {}
func test() {
  foo(a: 1, ) // should give bbb:
}
1 Like

There's one other option for that:

@inlinable
public func with<T>(_ /* consuming */initial: T, update: (inout T) throws -> Void) rethrows -> T {
    var value = initial
    try update(&value)
    return value
}

// All properties mutable, but they don't need to be optional
// if there's a default value. The value itself ends up immutable.
let user = with(UserRecord()) {
    $0.lastName = "Doe"
    $0.firstName = "Jane"
    $0.department = "R&D"
    // …
}
1 Like

@Max_Desiatov Given the feedback from this pitch, do you still want to pursue this? I'm thinking we should probably split the original pitch and destructuring into its own pitch. Generalized destructuring is something that is missing in Swift and should probably be pitched separately since it's useful on its own.

Thanks for following up, I've reached the same conclusion: this has to be split and destructuring has to be pitched separately. I have currently in mind something I call "pattern matching properties", which is able to match against any public property, either stored or computed:

enum E {
  case one(Int, Int)
  case two
}

struct A {
  let e = E.two
  let w = "test"
  var x = 5
  var y: Int { return x + 1 }
  var z: (Int, Int) { return (x, y) }
}

let { x, y, z }  = A()
// `x` is 5, `y` is 6, `z` is (5, 6)

I've seen in this thread a suggestion to utilize tuple destructuring syntax for this and tying in with possible ExpressibleByStringLiteral. I'm convinced it's not the best possible approach when considering possible "deep destructuring":

let { w, z: (x, y) } = A()
// `w` is "test",  `x` is 5, `y` is 6

If we used the tuple destructuring syntax for both tuple matching and property matching, this wouldn't look as readable:

let (w, z: (x, y)) = A()
// does A() return a tuple? because it looks like a tuple with this syntax

Using {} syntax for "deep destructuring" in all pattern matching scenarios should work pretty well. Note that we didn't use enum destructuring above, which I think shouldn't be allowed in usual let/var assignment contexts. But should be allowed for case let pattern matching:

switch A() {
case let { x, y } where x == y:
  print("main properties equal")
case let { e: .two(x, y) } where x == y:
  print("enum associated values equal")
default:
  print("other unmatched cases")
}

If anyone likes this approach, please let me know and I'll start preparing a thorough separate pitch for this. Thanks.

3 Likes

Yeah I like this approach. I think the details should be hammered out in its own pitch thread though. I already have some ideas on how destructuring and pattern matching might interact.

1 Like

I agree with the split and am looking forward to the destructuring thread. I think this will make a great proposal!

Yeah, I think having a more succinct way to forward parameters with a matching argument label would be great across the board.

6 Likes

+1. This is a very common scenario. Clarity would be increased by eliminating the redundancy.

2 Likes

I would love to be able to omit parameter names in cases like CGSize(width: width, height: height), as well as support for destructuring assignment, like let (width, height) = size. What's the status of this pitch?

1 Like

In my opinion

let (width, height) = size

would imply that size is a tuple, while

let { width, height } = size

would be more fitting as it mirrors the curly brackets used in non-tuple type declarations. Nevertheless, I haven't had any capacity to develop a prototype for this. Anyone willing to do so would be very welcome.

3 Likes

I think a big problem with the ergonomics for compiler fix-its for a problem like this is that as a developer writing new code you basically always want the compiler fix-it to be applied ... its quite annoying to write something and then select the fixit via the keyboard/mouse ...

If swift had a concept of a 'fix-it that is always applied' -- and took into account this functionality during language design as a legitimate problem-solving tool - then the experience around something like this could be solved for in way that avoids compromise requirements ...

I'd love to see the language take on some new explicit assumptions as part of its evolution capability ...

  1. an automatic code formatter (with essentially no configuration options) will always be run on all swift code
  2. the code formatter will be run on file-save

The language could then turn things like 'required ordering' into the best possible developer experience -- 'you usually don't have to write the properties in order but they will be saved in the right order'

1 Like

Up in the history @Max_Desiatov says we should separate the initialization and destructuring into separate forum topics, but I think we have them mixed up still.

I could not find a separate forum thread for destructuring but I kind of skimmed quick through the search results for "destructuring" in the forum and did not go much past that. sorry if I missed it.

This is only about destructuring but there is symmetry between initialization and destructuring. I have been thinking about destructuring structs and wrote part of a pitch in the background. I was thinking basically that this might sensibly look like:

struct S {
    let x: Int
    let y: String
}

func tryS(arg: S) {
    switch(arg) {
    case let S(x: j, y: k):
        print(j, k)
    case _:
        print("")
    }
    let S(x: j, y: k) = arg // j = x, y = k
}

by analogy with enum cases. In the switch it's a case let using the default initializer. for an assignment the default initializer goes on the left side of the equals sign (imprecisely in terms of the grammar of the language.)

I think that's pretty intuitive but I got hung up trying to work out the details in grammar and in implementation.

To use the initializers for construction and destructuring is symmetrical, which is nice. But initializers have a bunch of rules, and don't include computed properties, etc.

I was thinking about an initializer-like syntax that accepted computed property names as well (I think this was discussed above in a different way). But the labels in an initializer or function call are not from the same domain as the property names really. I think they're compatible but there might be edge cases, particularly if you try to unnest them. This might be fixable but I am concerned about it now before I analyzed it completely.

It also complicates the grammar further, because you have a new thing that can appear on the left of the equals, and it looks like an initializer but is not exactly, which might get intricate.

A third sticking point was that it applies to value types but not reference types, (presumably), so you lose some symmetry there. although it could work for reference types as well, I think.

The proposed syntax above puts braces on the left of the equals sign with matching based on declaration order, I think. The syntax is easy but a little bit bare. I feel like just brackets is too minimal and a little more should be there for the programmer to see what's happening.

What do people think about destructuring with initializer-like syntax, with labels matching public lets and public vars, in declaration order in the struct? It's not the same as any initializer if there are computed properties but it's reasonably clear and checkable. (One rough edge I can think of is where the visibility of properties is different--I said public properties above to smooth that but there might still be an edge case.)

That does not help with long initializer lists for complex structs, which was the original discussion point, but on its own would be useful.

The symmetry is not always there, sometimes you'd want to destructure against computed properties. Or if you have a struct with 10 fields, you'd only want to destructure two of them, consider:

struct S {
  let x: Int
  let y: String
  let z: Bool

  var a: Double { 42.0 }
  var notZ: Bool { !z }
}

Say you're only interested in y and notZ, how would you access those in your proposed initializer syntax? In my head (w/o modelling this after initializers) it would be something like:

let s = S(x: 42, y: "42", z: true)
let { notZ, y } = s
// desugared as:
// let notZ = s.notZ
// let y = s.y

A nice thing about it is that order shouldn't matter, while order does matter for initializer arguments.

By analogy with enums, it would look like

let S(x: _, y: _, z: j, a: k, notZ: _) = arg 

which would get you j and k from s.z and s.a (one intrinsic and one computed) and ignore the others. That's kind of clunky but could work. It is a problem with this form that it's so verbose though.

In the brace syntax, the label names have to match the names of the properties, right? That would mean that you could not have another variable with the name of a property in the same scope--imagine if there was a let y : UIView = currentView() above the let s above--there'd be no way to extract s.y there. The brace syntax is very concise though.

I think for the case where there is a name collision, it would be for one or two property names, and then you'd want to be explicit and just bind those directly. The logic is that these cases are rare enough, so you get conciseness for the vast majority of cases. And for the few rare you wouldn't want conciseness in the first place, then there's no strong need for the destructuring sugar:

let s = S(x: 42, y: "42", z: true)
let y = currentView()
let { x, notZ } = s
let newY = s.y

One other option could be adopting square brackets instead of braces, alluding to dictionaries then:

let s = S(x: 42, y: "42", z: true)
let y = currentView()
let [x, notZ, newY: y] = s

The fact that Swift uses square brackets for both dictionaries and arrays makes me subconsciously parse that as almost an array at first, so maybe that's not the best idea. After all we could also consider braces with colons for name collisions:

let s = S(x: 42, y: "42", z: true)
let y = currentView()
let { x, notZ, newY: y } = s

It occurs to me that in the non-switch situation you can just do:

let (a, b) = (s.x, s.y) 

which is straightforward and should be easy to desugar, or just optimize away.

What I was really focused on was the case let in switch to bind some variables in a case, because that has asymmetry with enum right now.

The first of these is more concise:

switch (s) {
case {x, oppoZ} where x < 43 && oppoZ: 
    ... using (x and oppoZ here)
case let {x, oppoZ}: 
    ... (using x and oppoZ here)
case {oppoZ} where !oppoZ:
   ... 

vs

switch (s) {
case let S(x: x, y:_,  z: _ a:_, oppoZ: oppoZ) where x < 43 && oppoZ: 
    ... using (x and oppoZ here)
case let S(x: x, y:_,  z: _ a:_, oppoZ: oppoZ): 
    ... (using x and oppoZ here)
case {oppoZ} where !oppoZ:
   ... 

obviously. And if you add some fields to S (or rename one like I did there) the switch will not compile any more with the second syntax. It's consistent with other things in Swift for it to fail to compile though. And I sort of like listing out all the fields, but possibly I am weird.

When I really started to analyze this things got complicated, because there are interactions and edge cases. The grammar for the language is already pretty complicated and any choice here makes it more complicated.

Anothing thing to consider is that if you do allow computed properties to appear in your proposed initializer-like syntax, computed properties can be added in extensions. If an extension with a computed property is in scope, does that require you to add that property to the initializer destructuring argument list? What if this is an extension private to a file, would how you use destructuring change in the same module, depending on which file it and the extension are in?

That's a good point. Extensions make a lot of things more complicated.

I was thinking only public (which I guess means globally available) lets and vars would be destructurable but extensions can add new public properties obviously.

The ordering of properties is also harder to reason about with extensions.

In general the init-like syntax is desirable for symmetry but as you say, it is not really symmetrical, and also difficult to implement for all these reasons. I worked through some of this writing part of a pitch and got stuck. I wanted to get it out there for people to look at, even though I know it has some problems, in case somebody had a breakthrough and found the underlying symmetry.

1 Like

I don't think it's that far off. Its semantics resemble a capture list, where we also use square brackets, but = instead of a colon.

2 Likes