Variadic parameters passing

This example is also correct, just not the behavior you're imagining it has. Let's desugar the variadic parameters by hand:

func test(_ arguments: [Any], execute: ([Any]) -> Void) {
    execute([arguments])
}

test([1, 2, 3], execute: { print($0) })

This outputs [[1, 2, 3]].

The type of variadic is just [Int]. There's no such type as Int.... Int... declares a parameter with a type of [Int] together with a special flag set that changes the behavior of the function application at the call site.

3 Likes

I wonder why are you saying that? I expected this behaviour! :slight_smile:
so "test([1, 2, 3], execute: ... )" outputs "[[1, 2, 3]]" and test(1, 2, 3, execute:) outputs "[1, 2, 3]", totally as I expected.

I'm suggesting we introduce another special flag that distinguishes "Int..." and [Int] inside the function, to get to this behaviour:

1 Like

This would be a breaking change, because of things like this:

struct G<T> { init(_: T) {} }
func foo(_ variadic: Int...) -> G<[Int]> { return G(variadic) }

I think once same-element requirements are implemented, parameter packs ought to be able to replace most usages of variadic parameters, so it's going to be difficult to justify making any further additions to the legacy feature, especially if they involve major extensions to the type system.

2 Likes

To clarify my point further:

func foo1(_ existential: Any...) { foo2(existential) }
func foo2(_ existential: Any...) { foo3(existential) }
func foo3(_ existential: Any...) { print(existential) }

func bar1<T>(_ generic: T...) { bar2(generic) }
func bar2<T>(_ generic: T...) { bar3(generic) }
func bar3<T>(_ generic: T...) { print(generic) }

func baz1(_ int: Int...) {
    baz2(int) // πŸ›‘ Cannot pass array of type 'Int...' as variadic arguments of type 'Int'
}
foo1(1, 2, 3) // [1, 2, 3]
bar1(1, 2, 3) // [[[1, 2, 3]]]

What I found somewhat unexpected is that the three behaviours above are all different...
My personal vote would be for how "Any..." currently works, YMMV.

1 Like

But that's the bug! (And this kind of ad-hoc behavior is why features like variadic parameters, dynamic Self, etc are bad. If it's just based on some magic flag or type kind that has to be checked and handled specially in certain places, someone will invariably forget.) The behavior in the generic case is the expected behavior. I missed the distinction when reading the original thread.

1 Like

Huh. Bear in mind that some of us (at least myself!) consider this behaviour the "right" one...
And that fixing this "bug" would be a breaking change...

Something similar happened with rethrows, and just like rethrows, I'm happy to leave this alone then :slight_smile:

FWIW I'm with @tera here - it's intuitive and simple for variadics to just work; to be able to pass a variadic argument as a variadic argument, etc. I've always found Swift to be a bit of nightmare when it comes to variadic arguments, because [in Swift] they're so awkward and limited currently, and I actively avoid using them for that reason.

Variadic generics might be technically a superset or superior or whatever, but they're substantially more conceptually & syntactically complex, and I've struggled to use them in real-world code (even aside from them being rather hard to use at all, it always seems to be the case that I hit some known limitation in the current implementation, that is a show-stopper for each use case I've tried - the relatively recent addition of for-loop support is great but addresses only one such hole).

In contrast, Python makes variadics super simple (it requires explicit splatting, but that's fine - I'm not sure it's necessary, but it's not hard nor confusing).

So I don't know what the solution is - e.g. supplant variadic arguments with [better] variadic generics or add explicit splatting or whatever - I just wish this rough patch of the language were cleaned up. Just the other day I needed a wrapper function over print and it was such a ridiculous pain for such a conceptually trivial task.

2 Likes

It would be pretty easy to add a β€œvariadic splat” operation, I think the only holdup is the lack of concrete syntax (and perhaps developer cycles).

I agree the parameter pack implementation needs some polish. I think once the gaps are filled in, it will be easier to form a coherent mental model for how it all works.

3 Likes

I tried a few languages and found that C# matches my intuition of how varargs should work:

using System;
					
public class Program {
	public static void foo1(params int[] values) {
		foo2(values);
	}
	public static void foo2(params int[] values) {
		foo3(values);
	}
	public static void foo3(params int[] values) {
	    for (int i = 0; i < values.Length; i++) {
	        Console.Write(values[i] + " ");
	    }
	    Console.WriteLine();
	}
	public static void Main() {
		foo1(1, 2, 3, 4);
	}
}
// outputs: 1 2 3 4

i.e. at the deepest level it's "an array of 4 ints" instead of being "an array of a single array of a single array of 4 ints".

2 Likes

Sure, but in my experience with C# this leads to problems when you try to pass an array as the only argument to a function that takes a params object[] and it accidentally gets splatted. Bonus points if it’s an optional array and you get a surprise NullReferenceException.

5 Likes

Yeah, it's not ideal there:

using System;
					
public class Program {
	public static void foo3(params object[] values) {
	    for (int i = 0; i < values.Length; i++) {
	        Console.Write(values[i] + " ");
	    }
	    Console.WriteLine();
	}
	public static void Main() {
		int[] integers = { 1, 2, 3, 4 };
		foo3(integers); // System.Int32[]   ONE WAY (IMHO the right way)
		object[] objects = { 1, 2, 3, 4 };
		foo3(objects); // 1 2 3 4           ANOTHER WAY (IMHO the wrong way)
	}
}
2 Likes