Variadic parameters passing

The following looks a bug, is it not?

func foo(_ v: Int...) {}

func bar(_ v: Int...) {
    foo(v) // 🛑 Cannot pass array of type 'Int...' as variadic arguments of type 'Int'
}

A non-ideal workaround:

switch v.count {
    case 0: foo()
    case 1: foo(v[0])
    case 2: foo(v[0], v[1])
    case 3: foo(v[0], v[1], v[2])
    case 4: foo(v[0], v[1], v[2], v[3])
    default: fatalError("TODO implement \(v.count) case")
}

or:

func foo(_ v: [Int]) {
    // move the functionality here
}
func foo(_ v: Int...) {
    foo(v)
}

although this leads to a potentially unwanted ability to call "foo" with normal array parameters:

foo([1, 2, 3]) // 🤔
2 Likes

Either this is a bug or not, I’d say this seems to be a desirable behavior to have. For instance, in Go you can pass it as foo(v...), which is highly convenient, and resolves disambiguity of array passing.

2 Likes

It's not a bug, it's a design flaw that we haven't been able to fix because no one can agree on a syntax for splatting an array into a variadic parameter list.

3 Likes

See, for example:

2 Likes

Given this, it sounds like the syntax the core team wants for splatting is:

func foo<T>(_ arg: T...) {
    print(repeat each arg)
}

Which isn't that bad, and I can't think of any reason it won't work. It's basically just an overload of repeat each that takes an array instead of a pack parameter.

The implementation in the pitch has probably bit-rotted though.

2 Likes

I think it could have worked without introducing any special syntax:

func foo(_ v: Int...) {  }

func bar(a: [Int], b: Int...) {
    foo(b)           // ✅
    foo(a)           // 🛑
    // even though:
    let c: [Int] = b // ✅
}

i.e. the type "Int..." carries some internal marker that helps to distinguish it from normal [Int].

Then you would need special syntax to not splat the array.

Array (or tuple) splatting while look related, is a different feature that could be considered separately. You don't need array splatting in the example above to pass "Int..." function parameter as an argument to a function that requires "Int..." parameter, and whilst "Int..." function parameter is treated as an array inside the function there could be some internal difference to treat it differently to a normal array (to not allow normal [Int] array passed to "Int..." function parameter).

It is when you go beyond simple case with Int… it becomes an issue. Say, we want a variadic parameter to be a protocol that an Array can confirm too. Now there is no way to know either we want to pass an array as a single argument, or expand to be a multiple arguments, and that’s why syntax for this matters. Without that feature would be incomplete and narrowly applicable.

1 Like

I believe this is an anti-feature and should not be allowed...

I don't see any issue in your example:

protocol P {}
extension Array: P {}
extension Int: P {}

func foo(_ v: P...) {}
func bar() {
    foo(1, 2, 3)        // ✅ (three parameters passed)
    foo([1, 2, 3])      // ✅ (one parameter passed)
    foo([1], [2], [3])  // ✅ (three parameters passed)
}
1 Like

Assume that these variadic parameters are collected into an array to later be passed to the function, like building a format string. You now limited in this application of an array to collect arguments.

BTW, oddly that this example will compile right now and print accordingly results:

protocol P {}
extension Int: P {}
extension Array: P {}

func foo(_ v: P...) {
    print(v)
}

func bar(_ v: P...) {
    foo(v)
}

bar(1, 2, 3) // will output [1, 2, 3]
bar([1, 2, 3]) // will output [[1, 2, 3]]
bar([1], [2], [3]) // will output [[1], [2], [3]]

While when you are passing concrete Int... it won't.

And with generics it compiles too, yet output differs:

func foo<T>(_ v: T...) {
    print(v)
}

func bar<T>(_ v: T...) {
    foo(v)
}

bar(1, 2, 3) // will output [[1, 2, 3]]
bar([1, 2, 3]) // will output [[[1, 2, 3]]]
bar([1], [2], [3]) // will output [[[1], [2], [3]]]
1 Like

Indeed. There are typos in your example but after I fixed those, namely:

extension Int: P {}
extension Array: P {}

it works with P..., and works the way I would expect it to work.

Which is exactly what I suggest to treat "a bug" and fix so it works like it now works with "P..." above :slight_smile:

2 Likes

Already corrected there, thanks for pointing :slight_smile: Got confused myself while trying several options.

Yep, now it is really seems like something does not right in here...

1 Like

Wow, good find.

Filed as Existential type unexpectedly enables splatting behavior with variadic parameter · Issue #73925 · apple/swift · GitHub

4 Likes

Hope it won't break anything, since the idea to check protocol and generic came from similar use in a small util I've written around Swift 3 and remembered that the initial case has worked there, so this behaviour has been around for a while I guess.

From my POV the current behaviour of "Existential..." is expected, logical and actually useful!

And the current behaviour of "generic..." is at least questionable. Consider that instead of:

func foo<T>(_ v: T...) {
    print(v)
}
func bar<T>(_ v: T...) {
    foo(v)
}

I use a seemingly equivalent:

func foo<T>(_ v: T) {
    print(v)
}
func bar<T>(_ v: T) {
    foo(v)
}
func foo<T>(_ v1: T, _ v2: T) {
    print(v1, v2)
}
func bar<T>(_ v1: T, _ v2: T) {
    foo(v1, v2)
}
func foo<T>(_ v1: T, _ v2: T, _ v3: T) {
    print(v1, v2, v3)
}
func bar<T>(_ v1: T, _ v2: T, _ v3: T) {
    foo(v1, v2, v3)
}
// and so on up to N parameters

The result should be the same, right? No... In this case it will be different:

1 2 3
[1, 2, 3]
[1] [2] [3]
1 Like

But this is not equivalent. The direct equivalent to

func bar<T>(_ v: T...) {
  // body
}

for exactly two parameters is

func bar<T>(_ v0: T, _ v1: T) {
  let v = [v0, v1]
  // body
}

I suggest we have a difference between [v0, v1] and _ v: T...

e.g.:

func foo(_ variadic: Int...) {
    var variadic = variadic // just to make it var
    var array: [Int] = variadic // ✅
    variadic = array // 🛑 Error
}

as if Int... (variadic) acts as a "subclass" of [Int] (array).

It already kind of is:

foo([1, 2, 3]) // 🛑
1 Like

Note if that's considered a bug it would be a breaking change to fix it:

func test(_ arguments: Any..., execute: (Any...) -> Void) { // ✅ ok on all godbolt Swift versions
    execute(arguments)
}
2 Likes

It's working as expected because bar2() calls foo2() with T := Array<T>. This makes it clear:

func foo2<T>(_ x: T...) { print("\(T.self), \(x)") }
func bar2<T>(_ y: T...) { foo2(y) }

bar2(1, 2, 3)

If we desugar the variadic parameters by hand:

func foo2<T>(_ x: [T]) { print("\(T.self), \(x)") }
func bar2<T>(_ y: [T]) { foo2([y]) }

bar2([1, 2, 3])

EDIT: Ok, I misunderstood your original bug report because you started with two examples that work as expected. Yes, it's a bug that the argument value is not wrapped inside of an array here:

protocol P { }
extension Int: P { }
extension Array: P { }

func foo3(_ x: P...) { print("\(x.count) variadic argument(s) to 'foo3'") }

let x: [P] = [1, 2, 3]
foo3(x)
  (func_decl range=[/Users/slava/src/swift/var.swift:6:1 - line:6:31] "bar3(_:)" interface type="([any P]) -> ()" access=internal
    (parameter_list range=[/Users/slava/src/swift/var.swift:6:10 - line:6:19]
      (parameter "y" interface type="[any P]"))
    (brace_stmt range=[/Users/slava/src/swift/var.swift:6:21 - line:6:31]
      (call_expr type="()" location=/Users/slava/src/swift/var.swift:6:23 range=[/Users/slava/src/swift/var.swift:6:23 - line:6:29] nothrow isolation_crossing="none"
        (declref_expr type="(any P...) -> ()" location=/Users/slava/src/swift/var.swift:6:23 range=[/Users/slava/src/swift/var.swift:6:23 - line:6:23] decl="var.(file).foo3@/Users/slava/src/swift/var.swift:5:6" function_ref=single)
        (argument_list
          (argument
            (declref_expr type="[any P]" location=/Users/slava/src/swift/var.swift:6:28 range=[/Users/slava/src/swift/var.swift:6:28 - line:6:28] decl="var.(file).bar3(_:).y@/Users/slava/src/swift/var.swift:6:13" function_ref=unapplied))))))

@xedin You might want to take a look.

2 Likes