Variadic Parameters that Accept Array Inputs

Variadic Parameters Accept Array Inputs

One of the main underlying problems with variadic parameters in Swift, is the inability to pass them around in functions or call them within functions.¹

Here is an example of what Swift should offer:

protocol ShoppingListItem { }

enum Furniture : ShoppingListItem {
    case Chair
    case Sofa
    case Bed
    case Table
}

enum CableType : ShoppingListItem {
    case MicroUSB
    case Lightning
    case USB_C
    case HDMI
}

extension String : ShoppingListItem { }

typealias ShoppingList = [ShoppingListItem]



func compileShoppingList<Item: ShoppingListItem>(_ arrs: Array<Item>...) -> ShoppingList {
    
    var returnArr = [ShoppingListItem]()
    
    arrs.forEach { arr in
        arr.forEach { item in
            returnArr.append(item)
        }
    }
    
    return returnArr
    
}

@discardableResult func makeShoppingList(with items: ShoppingList...) -> ShoppingList {
    
    var count = 1
    print("\nShopping List:", terminator: "\n\n")
    
    items.forEach { list in
        print("\tErrand #\(count):")
        count += 1
        list.enumerated().forEach { (index, item) in
            print("\t\t\(index + 1). \(item)")
        }
        print("\n", terminator: "")
    }
    
    let shoppingList : ShoppingList = compileShoppingList(items)
    //Above Error: In argument type '[ShoppingList]' (aka 'Array<Array<ShoppingListItem>>'), 'ShoppingList' (aka 'Array<ShoppingListItem>') does not conform to expected type 'ShoppingListItem'
    
    return shoppingList
    
}



let groceryList : [String] = ["Eggs", "Apples", "Orange Juice", "Cookies"]

let furnitureList : [Furniture] = [.Chair, .Bed, .Sofa, .Table]

let dongleList : [CableType] = [.Lightning, .USB_C, .MicroUSB, .HDMI]


makeShoppingList(with: groceryList, furnitureList, dongleList)

/*  Prints (If there is no error, which there is):
 
 Shopping List:
 
     Errand #1:
         1. Eggs
         2. Apples
         3. Orange Juice
         4. Cookies
 
     Errand #2:
         1. Chair
         2. Bed
         3. Sofa
         4. Table
 
     Errand #3:
         1. Lightning
         2. USB_C
         3. MicroUSB
         4. HDMI

*/

Main Problem

The main problem that this brings up is the inability to use methods/functions that have inputs of variadic parameters – for the most part¹ – within other methods/functions.


Proposed Solution

Variadic parameters should accept Array (and maybe Set) as a valid inputs, so to make variadic parameters easier to use and more passable between functions.


Footnotes:

¹. mainly passing parameters from the larger function to the function with a variadic parameter within it.


I definitely agree with the proposal. I think a simpler example would be as follows:

func f(_ x: Int...) -> [Int] {
    return x
}

let a = f(1, 2, 3)
let b = f(a)  // error: cannot convert value of type '[Int]' to expected argument type 'Int'

It seems rather odd that the returned input to a variadic function can't be used as input to the variadic function.

There have been a lot of proposals about this over the years. One alternative solution (forgive me for not giving credit here I found a thread about it and another one) would be making variadic syntax just an alternative way of writing an argument to a function. I can think of a couple of different ways of accomplishing this, here's a sketch of one:

func f(_ s: @variadic Set<Int>) { … }

let s: Set<Int> = [1, 2, 3]

f(s)         // current syntax with set
f([1, 2, 3]) // current syntax with array literal
f(1, 2, 3)   // variadic syntax

Then Int... can just become syntactic sugar for @variadic [Int], and perhaps be deprecated if that is considered desirable. This would allow you to directly construct any ExpressibleByArrayLiteral type in cases where something other than an Array is required, and automatically handles the issues with the incompatibility between Int... and [Int] in cases like the ones mentioned in this thread.

Edit: Now I've found the threads about this, I remember that there are some issues around ambiguity here that would have to be solved, but I don't think it's fatally flawed.

4 Likes

Right now, the work-around is to write both functions manually.

f(_ x: Int...) -> Int {
    return reduce(x, +)
}

f(_ x: [Int]) -> Int {
    return reduce(x, +)
}

If the only change made to variadics was to synthesize the second function automatically, that would be an ergonomic win. In my opinion.

2 Likes

I think it can be frustrating to work with variadics when arrays start getting involved. I do, however, advocate for thinking about variadics as semantically different to array's. One way (currently) to think about them as different is that their (Variadics) count is known at compile time. This can become a very useful thing to know later on (Value Generics, Compile Time Evaluation). We could expose this count thats known at compile and use it as a value constraint.

See this thread on not accepting 0 labeled arguments.

2 Likes

If we ever get fixed-length arrays, we could certainly convert variadics to that type. I think the benefits associated with knowing the count are orthogonal to whether the compiler allows some kind of automatic coercion between arrays and variadic parameter lists.

This seems redundant, if a variadic function is used as array in a function, then it should also be able to take an array as an input. You can always overload the function, but it would be much more ideal if the variadic parameter itself was just more flexible.

What if Variadics where their own Collection type?

If we had this, we could use Type... and also Variadic<Type> as the same input. This would solve many problems that have to do with the passing and use of variadic parameters.

2 Likes

It is redundant, but ofttimes, the actual implementation of a feature requires it. To be able to pass either a parameter list or an array to the same function requires special handling. The question is, how are the parameters handled today, and what are the minimal set of changes required for the compiler to allow an array to be passed instead?

The collection would have to be ordered, since the compiler could never make the assumption that the function doesn't care. So you've really just reinvented Array. So just make Type... shorthand for [Type(), Type(), Type(), ...] and be done with it.

How does this resolve the ambiguity of

func doAThing(with items: Any...) -> [Any] {
    return items
}

let res = doAThing(with: 1, 2, 3)
let res2 = doAThing(with: res)

Does the second call to doAThing receive three arguments (1, 2, 3, splatting res), or only one ([1, 2, 3], since res can be promoted to Any)? Or is this now a compilation error (which is potentially source-breaking)?

I've been bitten both ways in other languages which support argument splatting in this way; it'd be nice to offer a less ambiguous and surprising solution.

2 Likes

@itaiferber, That is why I think that maybe we can make a flexible Collection type called Variadic, or even a Variadic type cast for Array. This would prevent this issue, what do you think?

Now that SE-0213 has passed, you could solve this by making varargs their own logical type that implicitly converts to (but not from) [Element] with initializers that take an [Element]. The type itself will probably have to remain compiler magic for a while, but that shouldn't be too big of an issue.

// Probably predefined in the standard library. _VarArgs is not meant for direct user access
// because it's wicked black voodoo magic. The function name is just a strawman.
@inlinable
public func splat<T>(_ array: [T]) -> T... {
    return _VarArgs(array) // Converting init.
}

func doAThing(with items: Any...) -> [Any] {
    return items // items implicitly converts from Any... to [Any]
}

let res = doAThing(with: 1, 2, 3)
let res2 = doAThing(with: res) // returns an [Any] whose sole element is an [Any]
let res3 = doAThing(with: splat(res)) // returns the same thing as doAThing(1, 2, 3)
1 Like

I'm not sure that would resolve the issue here on its own. Array itself here isn't really relevant — the issue is that Variadic<Any> is implicitly convertible to Any:

func doAThing(with values: Any...) -> Variadic<Any> {
    return values
}

let variadic: Variadic<Any> = doAThing(with: 1, 2, 3)
let huh = doAThing(with: variadic /* is this converted to Any, or splatted? */)

We could decide that the semantics of variadics are special — passing in a Variadic<Any> (however this might be spelled in reality) into a function taking Any... could more strongly bind the contents of the variadic rather than passing the collection itself.

My question then becomes, what happens when you do this:

let variadic1 = doAThing(with: 1, 2, 3)
let variadic2 = doAThing(with: "foo", "bar", "baz")
let variadic3 = doAThing(with: variadic1, variadic2)

Are variadic1 and variadic2 splatted in there, or are they passed in verbatim?

  • On the one hand, we could decide that if the type matches the argument type exactly (Variadic<Any> == Variadic<Any>), we splat the contents, such that doAThing(with: variadic1) passes in the contents of the variadic, while doAThing(with: variadic1, variadic2) pass the collections in as-is

  • On the other hand, we could decide that we never splat automatically and always prefer the upcasting rules, and if you want to splat, @Nobody1707's suggestion is an explicit way to do it (i.e. you'd have to write doAThing(splat(variadic1)) to pass in its contents). We could even special case the syntax with something like *vars so you can express all of the following:

    1. doAThing(with: variadic1, variadic2) /* no splatting */
    2. doAThing(with: *variadic1, variadic2) /* splat the contents of variadic1, pass in variadic2 verbatim */
    3. doAThing(with: variadic1, *variadic2) /* and vice versa */
    4. doAThing(with: *variadic1, *variadic2) /* splat everything */

There's a sort of self-consistency to both approaches:

  • The first prioritizes type-level consistency: given an exact type match, no upcasting should be necessary [at the cost of making doAThing(with: variadic1) do something inconsistent with doAThing(with: variadic1, variadic2)]
  • The second prioritizes call sites doing the same consistent thing, ignoring type matching [doAThing(with: variadic1) behaves the same as doAThing(with: variadic1, variadic2)]

Because we don't have a way to fully express this today, the current behavior matches #1 (with Arrays being imperfect type matches):

func doAThing(with values: Any...) -> [Any] {
    print(values.count)
    return values
}

let a1 = doAThing(with: 1, 2, 3) // prints 3
let a2 = doAThing(with: a1) // prints 1

But there is no current precedent for what would happen here should we be able to return Any...


For what its worth, I think we can come up with a good solution either way — these are just edge cases that immediately come to mind that I think a pitch should address. :slight_smile:

1 Like

I guess the real question here is: which is more obvious to the people reading the code. Manual splatting:

let a1 = doAThing(with: 1, 2, 3) // prints 3
let a2 = doAThing(with: a1) // prints 1
let a3 = doAThing(with: splat(a1)) // prints 3

Or manual casting to Any:

let a1 = doAThing(with: 1, 2, 3) // prints 3
let a2 = doAThing(with: a1 as Any) // prints 1
let a3 = doAThing(with: a1) // prints 3

My gut instinct is that the former is more obvious than the latter, but there's an argument both ways.

1 Like

Why not scrape variadic arguments, it’s not hard to type .

I think that in let variadic3 = doAThing(with: variadic1, variadic2), variadic1 and variadic2 should be considered as the own collections, Array. But yes, I do think it is a good idea to have a splat¹ – as an extension and even a function – available for Variadics.


¹. We could honestly get this to be available for any Collection, this so we can splat each of element of any collection down to its individual elements as now type Variadic<T>.

My thought is that Any... should be sugar for Variadic<Any> and that the behavior should be similar to this:

func doAThing(with values: Any...) -> [Any] {
    print(values.count)
    return [Any](values)
}

let a1 = doAThing(with: 1, 2, 3) // prints 3
let a2 = doAThing(with: a1) // prints 1 (you have an array not an A...)

func doAnotherThing(with values: Any...) -> Any... {
    print(values.count)
    return values
}

let a3 = doAnotherThing(with: 1, 2, 3) // prints 3
let a4 = doAnotherThing(with: a3) // prints 3
let a5 = doAnotherThing(with: a4, a4) // prints 2

If Variadic<T> instances can only be created as you go into a function there should also be an explicit .splat so you can do this:

var a6 = doAnotherThing(with: a4.splat, 4) //prints 4

That said, being able to say something along the lines of a6.append(4) might be useful?

Variadic arguments were never about being easier to type, they've always been about being easier to read.

This is what I was gonna say as well.

Variadic arguments and arrays aren't the same. But variadic arguments and fixed-length arrays are the same. If/when they are implemented, we could work with variadics interchageably as that type.