Lifting the 1 variadic param per function restriction

Hi all,

I've got a quick pitch here on lifting the restriction that each function can only have a single variadic param, inspired by a conversation started by @anandabits on twitter. It turns out this restriction is now easy to lift, so without further ado:

Allow Multiple Variadic Parameters in Functions, Subscripts, and Initializers

Introduction

Currently, variadic parameters in Swift are subject to two main restrictions:

  • Only one variadic parameter is allowed per parameter list
  • If present, the parameter which follows a variadic parameter must be labeled

This proposal seeks to remove the first restriction while leaving the second in place, allowing a function, subscript, or initializer to have multiple variadic parameters so long as every parameter which follows a variadic one has a label.

Swift-evolution thread: Lifting the 1 variadic param per function restriction

Motivation

Variadic parameters allow programmers to write clear, succinct APIs which operate on a variable, but compile-time fixed number of inputs. One prominent example is the standard library's print function. However, restricting each function to a single variadic parameter can sometimes be limiting. For example, consider the following example from the swift-driver project:

func assertArgs(
      _ args: String...,
      parseTo driverKind: DriverKind,
      leaving remainingArgs: ArraySlice<String>,
      file: StaticString = #file, line: UInt = #line
    ) throws { /* Implementation Omitted */ }

try assertArgs("swift", "-foo", "-bar", parseTo: .interactive, leaving: ["-foo", "-bar"])

Currently, the leaving: parameter cannot be variadic because of the preceding unnamed variadic parameter. This results in an odd inconsistency, where the first list of arguments does not require brackets, but the second does. By allowing multiple variadic parameters, it could be rewritten like so:

func assertArgs(
      _ args: String...,
      parseTo driverKind: DriverKind,
      leaving remainingArgs: String...,
      file: StaticString = #file, line: UInt = #line
    ) throws { /* Implementation Omitted */ }

try assertArgs("swift", "-foo", "-bar", parseTo: .interactive, leaving: "-foo", "-bar")

This results in a cleaner, more consistent interface.

Multiple variadic parameters can also be used to streamline lightweight DSL-like functions. For example, one could write a simple autolayout wrapper like the following:

extension UIView {
  func addSubviews(_ views: UIView..., constraints: NSLayoutConstraint...) {
    views.forEach {
      addSubview($0)
      $0.translatesAutoresizingMaskIntoConstraints = false
    }
    constraints.forEach { $0.isActive = true }
  }
}

myView.addSubviews(v1, v2, constraints: v1.widthAnchor.constraint(equalTo: v2.widthAnchor),
                                        v1.heightAnchor.constraint(equalToConstant: 40),
                                        /* More Constraints... */)

Proposed solution

Lift the arbitrary restriction on variadic parameter count and allow a function/subscript/initializer to have any number of them. Leave in place the restriction which requires any parameter following a variadic one to have a label.

Detailed design

A variadic parameter can already appear anywhere in a parameter list, so the behavior of multiple variadic parameters in functions and intializers is fully specified by the existing language rules.

// Note the label on the second parameter is required because it follows a variadic parameter.
func twoVarargs(_ a: Int..., b: Int...) { }
twoVarargs(1, 2, 3, b: 4, 5, 6)

// Variadic parameters can be omitted because they default to [].
twoVarargs(1, 2, 3)
twoVarargs(b: 4, 5, 6) 
twoVarargs()

// The third parameter does not require a label because the second isn't variadic.
func splitVarargs(a: Int..., b: Int, _ c: Int...) { } 
splitVarargs(a: 1, 2, 3, b: 4, 5, 6, 7)
// a is [1, 2, 3], b is 4, c is [5, 6, 7].
splitVarargs(b: 4)
// a is [], b is 4, c is [].

// Note the third parameter doesn't need a label even though the second has a default expression. This
// is consistent with the current behavior, which allows a variadic parameter followed by a labeled,
// defaulted parameter, followed by an unlabeled required parameter.
func varargsSplitByDefaultedParam(_ a: Int..., b: Int = 42, _ c: Int...) { } 
varargsSplitByDefaultedParam(1, 2, 3, b: 4, 5, 6, 7)
// a is [1, 2, 3], b is 4, c is [5, 6, 7].
varargsSplitByDefaultedParam(b: 4, 5, 6, 7)
// a is [], b is 4, c is [5, 6, 7].
varargsSplitByDefaultedParam(1, 2, 3)
// a is [1, 2, 3], b is 42, c is [].
// Note: it is impossible to call varargsSplitByDefaultedParam providing a value for the third parameter
// without also providing a value for the second.

This proposal also allows subscripts to have more than one variadic parameter. Like in functions and initializers, a subscript parameter which follows a variadic parameter must have an external label. However, the syntax differs slightly because of the existing labeling rules for subscript parameters:

struct HasSubscript {
    // Not allowed because the second parameter does not have an external label.
    subscript(a: Int..., b: Int...) -> [Int] { a + b }

    // Allowed
    subscript(a: Int..., b b: Int...) -> [Int] { a + b }
}

Note that due to a long-standing bug, the following subscript declarations are accepted by the current compiler:

struct HasBadSubscripts {
    // Shouldn't be allowed because the second parameter follows a variadic one and has no
    // label. Is accepted by the current compiler but can't be called.
    subscript(a: Int..., b: String) -> Int { 0 }

    // Shouldn't be allowed because the second parameter follows a variadic one and has no
    // label. Is accepted by the current compiler and can be called, but the second
    // parameter cannot be manually specified.
    subscript(a: Int..., b: String = "hello, world!") -> Bool { false }
}

This proposal makes both declarations a compile time error. This is a source compatibility break, but a very small one which only affects declarations with no practical use. This bug also affects closure parameter lists:

// Currently allowed, but impossible to call.
let closure = {(a: Int..., b: Int) in}

Under this proposal, the above code also becomes a compile-time error. Note that because closures do not allow external parameter labels, they cannot support multiple variadic parameters.

Source compatibility

As noted above, this proposal is source-breaking for any program which has a subscript declaration or closure having an unlabeled parameter following a variadic parameter. With the exception of very specific subscript declarations making use of default parameters, this only affects parameter lists which are syntactically impossible to fulfill. As a result, the break should have no impact on the vast majority of existing codebases. It does not cause any failures in the source compatibility suite.

If this source-breaking change is considered unacceptable, there are two alternatives. One would be to make the error a warning instead for subscripts and closures. The other would be to preserve the buggy behavior and emit no diagnostics. In both cases, multiple variadic parameters would continue to be supported by subscripts, but users would retain the ability to write parameter lists which can't be fulfilled in some contexts.

Effect on ABI stability

This proposal does not require any changes to the ABI. The current ABI representation of variadic parameters already supports more than one per function/subscript/intializer.

Effect on API resilience

An ABI-public function may not add, remove, or reorder parameters, whether or not they have default arguments or are variadic. This rule is unchanged and applies to all variadic parameters.

Alternatives considered

Two alternative labeling rules were considered.

  1. If a parameter list has more than one variadic parameter, every variadic parameter must have a label.
  2. If a parameter list has more than one variadic parameter, every variadic parameter except for the first must have a label.

Both alternatives are more restrictive in terms of the declarations they allow. This increases complexity and makes the parameter labeling rules harder to reason about. However, they might make it more difficult to write confusing APIs which mix variadic, defaulted, and required parameters. Overall, it seems better to trust programmers with greater flexibility, while also minimizing the number of rules they need to learn.

38 Likes

+1000 yes please. I've been bitten by the restriction multiple times and the alternatives just never look or feel quite as nice.

Seems reasonable to maintain the 2nd restriction so I have no qualms there.

I can only imagine the way swift functions are going to change once this inevitably goes through. I say inevitably because even if this pitch fails, there will eventually be another that will eventually pass. It's really only a matter of time until swift supports multiple variadics per function. This pitch seems like a common sense addition in my opinion and is an excellent candidate for inclusion into the standard library.

3 Likes

While I don't think I would use it very often (if at all), it seems like an artificial restriction. Maybe there are APIs which can be better expressed this way. API authors should be free to do it if it makes sense for them.

I don't see this introducing any limitations on future language evolution, or any other downsides to accepting this. So basically my opinion is: "sure, why not?"

are there any such downsides which might cause this to deserve more intense scrutiny?

5 Likes

+1

Thanks for the pitch @owenv.

This change is great. Removing special cases helps make the language easier to grok and easier to wield.

+1...

@owenv what about subscripts? The labeling rules are different, but it should work there too.

Subscripts are intended to be supported too, I'll clarify that. I did find one potential issue with subscripts recently, which is that they don't enforce the labeling requirement for parameters after a variadic parameter. However, taking advantage of this results in a subscript that AFAICT can't ever be called. For example:

struct Foo {
	subscript(a: Int..., b: String) -> Int { // Allowed, but shouldn't be
		42
	}
}

let x = Foo()[1, 2, 3, ""] // doesn't work, the compiler assumes all args are for param #1

It might be acceptable to reject subscript declarations like this one, or make them a warning. Either way, it doesn't prevent supporting multiple variadic params, it just allows some nonsensical declarations.

4 Likes

+1, this should definitely work. Think it used to!

1 Like

Slightly off topic, I recall @Slava_Pestov fixing some enum related issues. I believe enums supported variadic payloads if some compiler flags were disabled.

Since we want to align enum cases more to functions, then they eventually need to support variadic payloads.

2 Likes

Yes, it used to work up to Swift 2 days at least.

Does anybody know when it stopped working and why?

A quick update on this: I've revised the proposal to make it clear the changes here apply to subscripts and initializers as well. I've also attempted to address the bug which currently allows one to write uncallable subscripts, which would be slightly exacerbated by this change if it isn't fixed. I'd appreciate feedback on the source compatibility section in particular, as I'm now recommending a very small break. If the consensus is that this isn't a good idea, there are alternate options listed which allow for supporting this feature in subscripts while maintaining full source compatibility.

3 Likes

+1 this would definitely make the APIs more consistent.

But in terms of labeling, I would suggest mandating labels for all the parameters excluding first parameter (irrespective of whether it is a variadic one or not) for better readability.

+1 This would be very useful in specific contexts.

I recently wrote a static subscript that fetches Core Data objects with given IDs, sorted in a certain way. I used a variadic parameter for the IDs, of course, but I had to use an Array for the NSSortDescriptors.
Class[id1, id2, id3, with: context, orderedBy: [sort1, sort2]]
This would obviously be easier to understand with multiple variadic parameters.
Class[id1, id2, id3, with: context, orderedBy: sort1, sort2]

+1
this would certainly help many APIs and remove many existing workarounds.

I have one doubt though, how would this work if the last variadic parameter type is a closure?

For the following function:

func variadicClosures(number: Int, closures: (Int) -> Int...) -> Int {
    closures.reduce(number) { (number, closure) in
        closure(number)
    }
}

It can be used without any problem like this:

_ = variadicClosures(number: 0, closures: { $0 + $0 }, { $0 * $0 })

But according to trailing closure syntax it should be usable like this:

_ = variadicClosures(number: 0) { $0 + $0 }, { $0 * $0 }
1 Like

Iā€™m definitely in favour.
Iā€™d only be wary of pushing the variadic arguments feature too much until we have a way of propagating it

This proposal doesn't change the existing, post SE-0279 behavior of how multiple trailing closures interact with variadic closure parameters. There's some additional discussion of this and examples in SE-0279 and variadic parameters, but basically, trailing closures and variadic parameters don't work very well together due to the trailing closure argument matching rules requiring a backwards scan, and this proposal neither improves nor worsens the situation.

2 Likes