Record initialization and destructuring syntax

Quite frequently there is a need to manually initialize structs/classes with a number of parameters that makes standard initializer syntax not ideal. Without Xcode autocomplete it's very hard to get order of parameter names right. And even with autocomplete if some of the parameters have defaults, this causes a number of possible initializer signatures to become too big:

struct ButtonNode {
  let textColor = UIColor.black
  let backgroundColor = UIColor.white
  let text = ""
  let width: CGFloat
  let height: CGFloat
}

// causes error: `width` should come before `height`
let button1 = ButtonNode(height: 100, width: 200)

Even worse, when you already have values defined for arguments in variables with same names as parameter names, you're forced to write this in a quite verbose repetitive way:

let height: CGFloat = 100
let width: CGFloat = 100
let button2 = ButtonNode(width: width, height: height)

Workarounds for this exist, but they require creation of custom operators, significant amount of boilerplate, and lead to obscure type-checking errors if a single little thing goes wrong in the use of operators:

With this type of code, you need to consider that any error reported by the compiler may point to the wrong location or otherwise indicate that the compiler has no idea at all what’s gone wrong. There are plenty of cases where I’ve needed to comment out parameter lines to narrow down the location of the problem.

A possible solution would be to allow a special "record initializer" syntax where order of arguments is not significant inspired by Rust:

// order of parameter names doesn't matter
let button1 = ButtonNode | height: 100, width: 200 |

// no need to repeat parameter names if they match variable names
let height: CGFloat = 100
let width: CGFloat = 100
let button2 = ButtonNode | height, width |

This would be a syntax sugar for an initalizer with matching parameter names and transform to let button2 = ButtonNode(width: width, height: height).

Important bikeshedding topic: why | symbol? The problem with {} brackets is they are already utilized by trailing closure syntax. It would be quite useful to allow both record syntax and trailing closure syntax to coexist in the same initializer expresion:

struct ButtonNode {
  // ... same properties with an additional one:
  let onTap: () -> ()
}

let button3 = ButtonNode | height, width | { print("button tapped") }

The next best option would be to use [] brackets for record initializer syntax, but these are already used for arrays and dictionaries and this option might look confusing:

// probably ok:
let button1 = ButtonNode [ height: 100, width: 200 ]

// quite confusing, is `[height, width]` an array?
let button1 = ButtonNode [ height, width ]

This "record initialization" syntax is quite useful by itself, but in addition a "record destructuring syntax" could be considered in a separate proposal (again, inspired by Rust, see "Destructuring Structs" section):

func area(button: ButtonNode) -> CGFloat {
  let | height, width | = button
  return height * width
}

If desired, another shorthand could be introduced:

func area(button | height, width |: ButtonNode) -> CGFloat {
  return height * width
}

func areas(buttons: [ButtonNode]) -> [CGFloat] {
  return buttons.map { | height, width | in height * width }
}

This destructuring syntax is also a good fit for pattern matching:

switch button {
case let | height, width | where height == width:
  print("square button")
default:
  print("rectangular button")
}

Thanks for reading, I hope this pitch makes sense. In my experience this syntax makes code a lot easier to read in Rust and it would be great if Swift allowed this as well.

4 Likes

+1

Javascript also has structuring/destructuring syntax in more recent versions, so there is definitely precedent.

Thanks! I wanted to mention JavaScript syntax here, but thought that JavaScript's precedent is not convincing enough with its use of dynamic duck typing. Rust's precedent though shows that this can be implemented in a statically typed language with a type system that's very similar to one we have in Swift.

I wouldn't like another syntax for such a fundamental task, although I'd appreciate being able to change the order of parameters (but that wouldn't work well with some labels...).

With methods (not only initialization) that make heavy use of default values, it can be quite tedious to add new customizations...

Thanks for the feedback. I'm not quite sure that utilizing existing initializer syntax would work if order of parameters didn't matter. Currently initializer calls are very similar to function calls, so it would be confusing if you could swap initializer parameter order, but not parameter order in a plain function call. Allowing this would lead to a lot of unintended consequences for simple function calls.

In this pitch I propose implementing this syntax only as a syntax sugar for initializers on classes and structs. It transforms to a matching initializer call and we could also restrict it only to types that have unambiguous initializers with unique set of parameter names, thus this type wouldn't be available for the syntax sugar:

// record syntax not allowed for this type:
struct S {
    let x: Int
    let y: Int

    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }

    // same as previous, but changes order, makes inference ambiguous
    init(y: Int, x: Int) {
        self.x = x
        self.y = y
    }

    // wow, I didn't even know Swift allows this
    // but this also restricts `S` from initializer syntax sugar
    init(x y: Int, x: Int) {
        self.x = x
        self.y = y
    }
}

One way or another, without this kind of solution a problem remains: there are many types that are either immutable and hold a ton of properties for storage (most struct use cases), or are mutable but frequently need a lot of options on initialization (most UIView subclasses in UIKit). Interesting to see to what degree people are ready to build workarounds in the article I mentioned above by Matt Galagher.

Another workaround I've seen is making immutable properties mutable only for the sake of reassinging them in arbitrary order. Especially for properties that don't have sensible default values this forces those to become optional as well:

struct UserRecord {
    var firstName: String?
    var lastName: String?
    var building: String?
    var street: String?
    var county: String?
    var state: String?
    var postcode: String?
    var department: String?
}

// all properties made mutable and optional only to allow this:
var user = UserRecord()
user.lastName = "Doe"
user.firstName = "Jane"
user.department = "R&D"
...

I'm convinced the problem is common enough to be resolved on the language level, especially as we have enough precedents in other languages for similar solutions working well.

1 Like

I personally would be against such syntax because it comes with a very high cost of readability, since in my eyes it's a little too cryptic. (I was referring to the syntax using the pipe | )

I don't fully remember, but the pitches about self re-binding solved a few problems of that particular area. However there is still an issue with trailing closures in initializers.

Other then that in unambiguous context I think I can accept a trailing closure syntax with re-binded self as a 'good enough' solution.

let point = CGPoint {
  height = 4
  width = 2
}
4 Likes

Good read, I was just looking for destructuring early today when I saw a similar thing done in Kotlin. Also, looking at your code samples, this is evident that something similar to C# data classes would be helpful here where initialising the object does not use constructor/init syntax but more like a Javascript Object way. like var str = ButtonNode { height = 5, width = 6 }

1 Like

I personally would be against such syntax because it comes with a very high cost of readability, since in my eyes it's a little too cryptic.

Thanks, I'd be very interested to get a prototype implementation in the compiler quickly, would be great to understand what's the consensus on the best syntax for this. Could you please suggest syntax that's more readable in your opinion?

more like a Javascript Object way. like var str = ButtonNode { height = 5, width = 6 }

That's a great analogy, unfortunately I don't think this syntax can be reused easily in Swift because it matches the existing trailing closure syntax. This also almost reads like a trailing closure with a comma between statements instead of a semicolon.

Some bikeshedding.

struct Test {
  init(x: Int = 0, x y: Int = 1, z: Int = 2, closure: () -> Int = { 42 }) { ... }
}

// There are ways to solve these problems, but the rules you need 
// to make that happen are not intuitive enough IMHO.
//
// 1. If all parameters have defaults, then the closure must contain
//    all specified parameters.
// 2. Since parameters must be different but not the labels you have
//    to use the parameters instead of the labels.
// 3. What does `self` rebind really mean in initiliazers?
// 4. If there is no trailing closure in the inializers you can ignore rule #1.
// 5. Referencing something from the outer scope is allowed:
let closure = { 0 }
let test1 = Test {
  x = 1
  y = 2 // it's not a second `x`
  z = 3
  self.closure = closure
}

let test2 = Test { x = 1; y = 2; z = 3; closure = { 0 } }

let test3 = Test { this in 
  this.x = 1
  this.y = 2
  this.z = 3
  this.closure = { 0 }
}

That's a great example, thanks. What do you think about the proposed destructuring syntax? Would it fit into the closure-like syntax that you've just proposed?

let { x, y } = test1

I'd rather have generalized destructuring into a tuple (actually it might be a good idea to allow instantiating with some kind of a tuple syntax - just play around with that thought).

let (width, height) = CGPoint.zero

1 Like

Remember also that you don't generally need to know the difference between stored and computed properties, and indeed across module boundaries you shouldn't know (unless the type came from C). I (personally) think that any destructuring syntax should treat stored and computed properties uniformly, and it's already a requirement that any initialization syntax actually go through an initializer.

3 Likes

Yeah, I don't think we need more syntax forms for constructing things. It would be nice to improve on the support for the implicit elementwise initializer to support default arguments, exporting it as public, and generating elementwise initializers for classes, as @anandabits and others have proposed in the past. In the comments on @Erica_Sadun's recent blog post about tuple initialization, the idea also came up of allowing labeled tuple initializers in type context, which I think is nice:

let p: CGPoint = (x: 1, y: 2)

As others noted, the key thing missing is destructuring syntax for pattern matching and let bindings. I think the labeled tuple syntax would be a nice candidate for this as well:

let (x: x, y: y) = p

switch p {
case (x: 0, y: 0): print("on the origin")
case (x: 0, y: _): print("on the y axis")
case (x: _, y: 0): print("on the x axis")
default: print("somewhere else")
}
20 Likes

Yes, I would much rather focus on some kind of explicit destructuring syntax that can be used with pattern matching. Too many times I've reached for trying to overload ~= based on a tuple and a type only to get complaints thrown at me by the compiler. It's one of the big holes in pattern matching imo.

I like the ExpressibleByTupleLiteral approach, but quite unsure about this one:

wouldn't this option be clear enough then:

let (x, y) = p

Please don't forget that the main point of the pitch is not to help with the simplest case like CGPoint with only 2 fields, but with bigger record-like objects, like UserRecord example above with potentially dozens of fields. In that case this could become quite verbose:

let (firstName: firstName, lastName: lastName, building: building, department: department...) = record
2 Likes

We don't need it, but I think making value types more ergonomic will encourage people to use more of them, which IMHO is good thing. I agree though that problems related to the memberwise initialisers are more pressing in this context.

(Compared with e.g. Haskell or also Scala, there is too much boilerplate involved in simple struct definitions for my taste)

I think there are really three things in this proposal:

  1. Relaxing the order restriction of named arguments: I think this would be a good thing everywhere... one benefit of named params in a lot of languages is not having to remember their order. But I don't know which design decisions went into this.
  2. Having syntactic sugar for identifierName: identifierName, which I guess is a common pattern
  3. Having syntactic sugar for let property = someInstance.property, which is probably also a common pattern.

For 2, one could also think of some alternate, less "cryptic" syntax. E.g. how about ButtonNode(width:, heigth:) as a shorthand for ButtonNode(width: width, height: height)? IMHO, that comes close in spirit to the Javascript approach (makeButton({ width, height })) .

2 Likes

We intentionally require preserving order (see SE-60), and it isn't clear to me that allowing for reordering really helps ergonomics in any meaningful way. Having a way to cut down on x: x boilerplate for same-name labeled arguments could be interesting, I agree. That could be generally applicable to labeled argument lists, though, not specific to initializer arguments. On the pattern matching side, in SE-155, we decided to allow case .foo(let x) as shorthand for case .foo(x: let x), and if we added pattern matching support for properties of structs or classes, it would make sense to me to allow that same shorthand.

13 Likes

And if we do a side by side comparison, I'm not sure that's really the case, at least for declaration and construction of named records:

-- Haskell
data Foo = Foo {
  x :: Bar,
  y :: Bas,
  z :: Zim
} 

let foo = Foo { x = x, y = y, z = z }

// Swift
struct Foo {
  var x: Bar
  var y: Bas
  var z: Zim
}

let foo = Foo(x: x, y: y, z: z)
4 Likes

You're right when it comes to records (I don't like records in Haskell). But Haskell allows you to write:

data Rectangle = Rectangle Int Int

which is really short and convenient. But yes, you wouldn't have labels in that case.

In Scala, you have:

class Rectangle(val width: Int, val height: Int)

which is more ergonomic and also more extensible (e.g. you can even do:

class Rectangle private(val width: Int, val height: Int)

and things like that)

(But this is going off a tangent)

1 Like