Record initialization and destructuring syntax

The main problem with ordering comes up again with big record-like objects. This is exacerbated by poor tooling support: autocomplete support is only available in Xcode, but even with autocomplete default arguments cause multiple initializers to appear with different sets of fields, where the order still matters. Please allow me to quote this great article from Cocoa with Love again:

extension UIView {
   init(
       alpha: CGFloat = 0,
       backgroundColor: UIColor? = nil,
       clearsContextBeforeDrawing: Bool = true,
       clipsToBounds: Bool = false,
       contentMode: UIViewContentMode = .center,
       exclusiveTouch: Bool = false,
       gestureRecognizers: [UIGestureRecognizer] = [],
       horizontalContentCompressionResistancePriority: UILayoutPriority = . defaultHigh,
       horizontalContentHuggingPriority: UILayoutPriority = .defaultLow,
       layoutMargins: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0),
       mask: UIView? = nil,
       motionEffects: [UIMotionEffect] = [],
       multipleTouchEnabled: Bool = false,
       opaque: Bool = false,
       preservesSuperviewLayoutMargins: Bool = false,
       restorationIdentifier: String? = nil,
       semanticContentAttribute: UISemanticContentAttribute = .unspecified,
       tag: Int = 0,
       tintAdjustmentMode: UIViewTintAdjustmentMode = .automatic,
       tintColor: UIColor = .white,
       userInteractionEnabled: Bool = true,
       verticalContentCompressionResistancePriority: UILayoutPriority = .defaultHigh,
       verticalContentHuggingPriority: UILayoutPriority = .defaultLow
       ) { /* ... */ }
}

The autocomplete feature in Xcode starts to get a little weird with this many defaulted parameters. Autocomplete issues might seem like a minor inconvenience but since the entire purpose of this construction code is supposed to be convenience, it merits consideration.

Another inconvenience is that Swift requires parameters be provided in-order. For long lists of properties, you’d probably need to carefully keep things in alphabetical order to make it work - which is a hassle since properties like these are not inherently an ordered concept.

This approach really becomes strained when we look at inheritance hierarchies. There’s as many as 30 properties you might set on a UITextField, plus 6 on UIControl plus the 25 on UIView. That’s over 60 parameters before we’ve included actions, delegates and oddities like how to handle mutability, all of which would likely increase (and in the case of mutability multiply) the number of parameters required.

That sounds like a bug we should fix. (I've noticed code completion behavior around argument labels is in general jankier than it could be, and would be a great general area to improve.) If you get the order of arguments wrong, the compiler will provide a fixit to put them in the right order, though.

1 Like

Even that is not that many whitespace characters removed from Swift:

struct Rectangle { var width: Int, height: Int }

It would be nice to maintain that concision when making common changes to the visibility of the initializer, and maybe provide an easy way to drop the initializer labels when they're obvious, I agree.

4 Likes

I guess you're right, I just never considered putting the whole struct declaration on one line. :slight_smile:

I'm quite convinced it's quite far away from ergonomics you have in languages where these record-like objects can be initialized with no required ordering. A compiler fixit requires IDE/editor integration to be conveniently applied. While in Rust/JavaScript I can write initialization values for struct/object fields in any order without looking at the initializer signature in Vim/Emacs/favorite editor and without any language plugin whatsoever.

You're still going to have to look something up to know all of the fields you need to assign, since you need to provide a complete set of fields in order to produce a value (in Swift and Rust at least, perhaps not Javascript). Having code completion integration to fill that set in seems useful regardless as soon as you have more options than can be conveniently remembered at once. I can believe that removing the ordering restriction may make things marginally easier, but it doesn't strike me as important as the other issues that you and others raised in this thread.

Absolutely, not debating that code completion is better than no completion at all. But a compiler error like

rename motionEffect to motionEffects at line 10

requires significantly less effort to fix than

rename motionEffect to motionEffects and move it from line 10 to line 25 exactly after parameter mask

when you have a ton of parameters already in a huge initializer call.

Besides, from a UX perspective, a list of property names in autocomplete looks much more readable than a huge list of initializers caused by all possible default parameter combinations. Some initializer combination possibly won't even fit in autocomplete list because of the length, but it's also much hard for a user to find a matching combination. Here's how autocomplete looks for long initializers in Xcode:

You can't see all of the parameters, in addition to the fact that it displays only one of all possible initializer variants. Even if all possible initializer variants were displayed, it would be very hard for a user to pick a correct one.

Just out of curiosity, why exactly are you creating such massive inits?

R is quite flexible in this respect, and this a big help when dealing with complicated functions:
When you have ten parameters with default values, it would be very annoying if you not only had to remember the name of each parameter (which is often easy, because of "standard" names), but also where it has to be put relative to other parameters.

1 Like

That specific UIView initializer is a superclass initializer that could allow something like this if a proper UIButton initializer was implemented (and if parameter name order didn't matter obviously):

let button = UIButton(text: "test",
                      backgroundColor: .black, 
                      tag: 999, 
                      clipsToBounds: true)
// etc

But arguably you could assign these properties after initialization. Where it gets more interesting is record-like value types that you'd prefer to keep immutable as much as possible:

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"
...

The only option you currently have is to either use the huge default UserRecord initializer where ordering matters and autocomplete is not great, or to assign it to a variable like in the example above, losing any immutability. In addition, arguments that aren't passed in initializer and don't have default values have to be made optional because of this.

With self re-bind the following function and example

precedencegroup LeftAssignmentPrecedence {
  associativity: left
  higherThan: BitwiseShiftPrecedence
  assignment: true
}

infix operator <- : LeftAssignmentPrecedence

@discardableResult
public func <- <T>(
  lhs: T,
  rhs: (T) throws -> Void
) rethrows -> T where T : AnyObject {
  try rhs(lhs)
  return lhs
}

let button = UIButton() <- {
  $0.text = "test"
  $0.backgroundColor = .black
  $0.tag = 999
  $0.clipsToBounds = true
}

would look like this:

let button = UIButton() <- {
  text = "test"
  backgroundColor = .black
  tag = 999
  clipsToBounds = true
}

Wouldn't this be convenient enough actually?

For value types you can define an overload that makes use of inout (but there were a few bugs related to that - not sure if they are already fixed or not).

1 Like

I hope it would, but unfortunately a lot of time values assigned in initializer code aren't compile-time constants like in trivial examples, but are passed from somewhere else. When that happens it could like this:

let button = UIButton() <- {
  text = text
  backgroundColor = backgroundColor
  tag = tag
  clipsToBounds = clipsToBounds
}

which is quite repetitive and error-prone. I wish we could write

let button = UIButton() <- [text, backgroundColor, tag: 999, clipsToBounds] 

Or even

let button = UIButton(text, backgroundColor, tag: 999, clipsToBounds)

allowing to mix both parameters with values that don't match the parameter name and those that do.

This is how it works in Rust perfectly well (except that parameters are enclosed in curly brackets {} instead of (), which is great because it doesn't require any custom operators or generics at all to work.

The example in the middle is already completely illegal in Swift:

var value = 42
value = value // error: Assigning a variable to itself

Great point, but then it's unclear what syntax should be used within that "initializing closure", self doesn't make much sense because it can't be captured lexically at that point. Unless we start allowing dynamic self like dynamic this in JavaScript closures, which is not something many people are willing to consider I guess :smile:

It seems to me that this could be achieved through reasonable small improvements to existing call syntax (having a shorthand for name: name redundancy, maybe reconsidering the order restrictions on defaulted arguments, maybe allowing the type to be inferred from context for an initializer call) instead of adding an entirely new syntax or cobbling together other new language features.

3 Likes

Great to hear that! Thanks for the clarification, initially it wasn't clear at all if order restriction could be relaxed, especially considering there was a proposal accepted that makes it more strict. I hope my points about subpar autocomplete UX for long initializer calls are still valid and ordering could be relaxed for initializers without default arguments as well.

I would be interested in seeing how much we can improve the autocomplete experience here. @blangmuir, do you know if this is a known issue?

Likewise, the main problem seems to be that long initializers just don't fit into the autocomplete list by width and adding only a few default arguments adds a combinatorial explosion of possible options. But even then without seeing the whole initializer list (because they're so long and numerous) it's hard to pick the correct one, so you end up adding parameter names one-by-one manually, which is obviously error prone and is exacerbated by ordering restrictions.

1 Like

Some gaps that I know of:

  • We have no "signature help" to show information about the current call, so once you've selected a completion you have no global view. This feature could be particularly helpful for long completions if it focuses in on the current and nearby parameters.

  • When you're trying to add an argument to an existing call, we do have some support for completing argument labels individually but it hasn't gotten much love. It is flaky about figuring out what the function is and when that happens tends to give no results. It also doesn't include anything except the argument label - it should really have the type and if available, the documentation.

  • We have no dedicated workflow for default arguments, we just generate two completions as if they were overloads.

Beyond that, there may be UI changes that could help, like showing the argument list of the currently selected completion vertically where there is more space.

You may know this already, but just to clarify: we don't add completions for all combinations of default arguments. We generate two completions: one with none of the defaulted arguments and one with all of them.

3 Likes