Variadic Parameters that Accept Array Inputs

A trivial example: sil hidden @output.doSomething(x: Int...) -> () : $@convention(thin) (@owned Array<Int>) -> ()

Ah, that makes life easier.

Why not just allow you to say #variadic(someArray) in the parameter list? It avoids wacky redesigns, and I think it’s probably rare enough that a slightly awkward syntax is no big deal.

The thrust of my question is regarding variadics. There have been calls for FixedSizedArray that have nothing to do with variadics, and there are obvious uses and optimizations that can be done with them. If you want to introduce FixedSizeSet for similar reasons, that would make sense. However, I don't see why you'd tie them to variadics. The input to a function must maintain order of parameters, even when they are gathered into a single collection. This calls for an array, not a set.

3 Likes

Ahh, I see. That's a good point. Yeah, I don't see why you'd want variadic sets then. At least if what we call "variadics" is exclusively "variable input to functions".

I mean, for how basic the concept is, it feels strange to tie it directly to something like a Set, which asks for uniqueness semantics in the inputs. But I could very well be wrong. @Wildchild9, how did you envision this?


Edit: that said, the interesting thing (semantically) from a function that you know recieves a VariadicSet argument, is that it implies the following property:

  • Let f: VariadicSet<T> -> U be a function.
  • Then you know that f(a, b) = f(b, a) for all pairs of inputs a and b. We know that f is commutative, without knowing anything about its implementation.
  • The same is true for any length of input. The output will not change if you change the ordering of the parameters.

This is very interesting, but out of my field of expertise. Maybe @codafi and @harlanhaskins have opinions on this specific topic.

Maybe slightly off topic, but "Variadic parameter cannot have a default value" is sometimes clumsy.

I don't know that this needs to be encoded in the type system, though. The documentation for the function has to convey the same information, because what the method does strongly implies how it uses the inputs.

Trivial examples

func avg(_ input: Int...) -> Float {
   return Float(input.reduce(0, +)) / Float(input.count)
}

By known what the avg() function does, you can deduce that the order of inputs doesn't matter, but you expect duplicate elements to be preserved.

func max(_ input: Int...) -> Int {
    var i = input[0]; input.forEach { i = max(i, $0) }; return i
}

Again, you can tell that the max() function does not care about order, and duplicates are effectively ignored. Whether this is because the arguments are gathered into a set, or because max(i, i) == i is not really an important detail.

func join(_ strings: String...) -> String {
    return strings.reduce("", +)
}

Here, the order obviously does matter, and there's no expectation that duplicates are removed. How this is implemented isn't important.

That is true, but usually you'd like to test those properties just to make sure some change didn't screw up one of the functions in the codebase.

The neat thing about something like this being embedded in the type system is that you wouldn't need to test those properties. The compiler would verify them for you. That's why I find it so interesting.

That said, if you mean that from a documentation standpoint it's not necessary because you can document it with the comments and the function itself, then you're absolutely right.

What does everyone think about manual splatting versus automatic splatting?

I think that manual splatting is the best option as it provides more flexibility and will help eliminate confusion surrounding using collections in variadic parameters.

  • This could be implemented with a function version, splat(someVariadic), and an extension version, someVariadic.splat()
1 Like

I'm for manual splatting.

Do you think that you should be able to splat any Collection?

Array splatting is the big win here, and moving from only being able to splat arrays to being able to splat arbitrary collections isn't source breaking, so I think we should leave out the more general version unless we have an actual use case for it.

I don’t think we should support all Collections. Supporting Array has no performance impact, since variadic arguments are already passed in an Array. Supporting all Collections means constructing a temporary Array (costs in performance) or redesigning variadic arguments to support any Collection (costs in design time, implementation time, and probably performance too).

3 Likes

I second this, for a different reason:

We have to assume that parameter order matters, even for variadic functions. Giving the ability to splat non-ordered collections will just lead to bugs that would otherwise be really obvious to avoid.

4 Likes

We should also ensure that an error message is thrown if someone does indeed input a variable holding Variadic<Int> that they need to splat it before putting it into a parameter holding Int... (Variadic<Int>).

Why would it be an error? Perhaps I'm missing something, but wouldn't Variadic, as proposed, be a fully-qualified type? Why would it be restricted in this way?

2 Likes

So @Avi, would you say that the only circumstances that require manual splatting should be those where the variadic parameter itself cannot hold a variadic values (check the following example)

// for the purpose of the argument,`doSomethingWithAnyCollection` or `doSomethingWithACollection <T: Collection>(_ arg: T)`
func doSomethingWithAny(_ arg: VariadicArray<Any>) { // could have also wrote Any...

    let count = arg.count
    print(count)
}

What would you say should happen if I do this:

let slightlyProblematicVariadic : VariadicArray<Any> = [3, true, "Marvin", "Sally", -12439.2349332, false]

doSomethingWithAny(slightlyProblematicVariadic)

What should be printed, 1 or 6?

What about if I do:

let slightlyProblematicVariadic : VariadicArray<Any> = [3, true, "Marvin", "Sally", -12439.2349332, false]
let anotherProblematicVariadic : VariadicArray<Any> = [false, "Joe", 23, "Allan", true, 47.4951]

doSomethingWithAny(slightlyProblematicVariadic, anotherProblematicVariadic)

What should happen in this case too?

The opposite. If the type can hold a variadic, it requires manual splatting to explicitly declare the desired behavior at the call site. If you don't do this, there would be no way to pass a VariadicArray as a single parameter to such methods.

You could make it a rule: VariadicArray is always splatted, and to avoid this, wrap the value in Array() at the call site. But, can this be optimized out at compile time? An explicit splat of VariadicArray is handled at compile time, because it directs the compiler to not wrap the VariadicArray into another VariadicArray. Forcing the call site to invoke Array() would either need special compiler magic, or it would happen at runtime.

I think in this case "6" would be printed, as you are passing a Variadic<Any> to a function taking a Variadic<Any>

In this case, I would say "2" would be printed, as you are passing Variadic<Variadic<Any>> to a function accepting Variadic<Any>.

2 Likes

That's what I would expect, too.

1 Like

Should a parameter of Int… accept a variable holding a VariadicSet<Int> as well as VariadicArray<Int>. But if a variable is not explicitly declared I think that it should be inferred to be a VariadicArray, leaving VariadicSets to require explicit type inference as is the same in the declaration of Sets and Arrays

aRandomVariadic = 1,2,3,4,5,6 //Inferred to be VariadicArray<Int>

Lastly, I think the shorthand, T… should be restricted to use only in functions as parameters that can reference VariadicArrays and VariadicSets. Like the equivalent of writing the hypothetical AnyVariadic in this case.

1 Like