Explicit array splat for variadic functions

My expectation for a function which can either take a single argument or an array is that the latter just puts the single argument into an array and calls the first - I can’t think of any good examples to do it differently.
@Haravikk3 idea would be a fundamental change on what variadics are, but required changes when using them would be trivial:
Everything would stay as it is, you just would replace all those magic “T…” with “@variadic [T]”.

I agree that something needs to be done about this, because I quite frequently find myself weighing the pros/cons between allowing variadics and also writing the boilerplate to allow both variadics for developer friendliness and arrays for more dynamic usability. I generally find myself doing something like this:

class HelloWorld {
    var names: [String]
 
    // This becomes ambiguous with the variadic, so just remove it
    // init(_ name: String) {
    //     names = [name]
    // }

    init(names: [String]) {
        self.names = names
    }

    convenience init(_ names: String...) {
        self.init(names: names)
    }

    func sayHello() {
        names.forEach { print("Hello \($)") }
    }
}

This allows me to write:

let hello = HelloWorld("Jacob", "Tim", "Steve")
hello.sayHello()

As well as use this dynamically:

class Goodbye {
    var hello: HelloWorld

    init(_ names: String...) {
        // Cannot initialize using the variadic since names is converted to an array
        hello = HelloWorld(names: names)
    }

    func sayGoodbye() {
        hello.names.forEach { print("Goodbye \($0)") }
    }
}

This is a poor example since in this case you would just subclass HelloWorld and add the sayGoodbye() function, however, it still demonstrates the principle that causes a lot of developers headache. We need splatting (or something else) to unify variadics and Collection or Sequence types. This would eliminate boilerplate code many of us are forced to use right now if we want both the variadic simplicity and array dynamic usability.

I also don’t like how we are limited to only 1 variadic argument per function. Solving the issue in this pitch would remove that restriction.

I’m in favor of auto-splatting variadics into their corresponding Collection/Sequence type (so that this could work with both Arrays and Sets), but that’s because I’ve only every seen that (since I’ve programmed in Ruby). I am definitely open to other suggestions that may possibly be better.

It would be nice to keep the ... syntax rather than needing to use an @variadic attribute at the function parameter site.

If we could turn variadics into something like this:

class HelloWorld {
    var names: Set<String>
    init(_ names: Set<String>...) {
    }
}

And then the compiler knows that names is either a Set<String> or a variadic to be converted to a Set<String> that would be great. If you wanted an Array<String> then you’d say [String]... instead.

This would be very useful. Sometimes you have a variadic parameter that you want to forward to another variadic function.

For example, say you wanted to define a conditional print function that only prints based on some condition:

func myPrint(_ args: Any...) { 
  if condition {
    print(args...) // hypothetical syntax
  }
}

Currently, there’s no way to write this without gross hacks.

9 Likes

I think most people are in favor of this (though clearly not all, as this thread shows!). However, the syntax is at odds with the one-sided range syntax added in Swift 4, and so someone has to figure out how to either disambiguate or what syntax would be good to use instead. (The other languages I know of use either array... or *array for this, or just array with no adornment at all.)

The next step, of course, is implementation. Unfortunately I don’t know enough about the expression type checker to say how tricky that is.

Is this really a collision with the one-sided range operator? The one-sided range operator is always used in a subscript isn’t it?

I don’t think there should be any ambiguity if one-sided ranges are always within [] and this would be outside of [] and at a function declaration.

That being said, if there does end up being a collision with the one-sided range operator, I think it would be acceptable to use a splat operator like * at the front of an array variable.

My other question though is, once we get splatting of some sort, what will we do about array vs variadic functions/initializers? Do we allow both to be declared or will one overwrite or collide with the other? Will there be instances where the developer does not want to allow a function to be used variadically? If not, then will there be the possibility of making compiler magic just say variadics always map to Collection/Sequence parameters?

No, it’s perfectly legal to use one-sided ranges as an infinite sequence:

zip(42..., ["a", "b", "c"]).forEach { print($0) }

A variadic parameter is declared with syntax that appears to claim it has type “T…”, for some T. It seems natural that if you have a parameter masquerading as type “T…”, you should be able use it as a value of type “T…”.

In other words, no “splatting” operator is necessary, and if such an operator existed then it would be actively detrimental to the understanding of the code.

Adapting Lance’s example:

func myPrint(_ args: Any...) { 
  if condition {
    print(args)
  }
}

Here we take a variable of type “Any…” and pass it to a function which accepts…an argument of type “Any…”. This should just work.

Notably, this functionality is essentially unrelated to the “tuple splatting” that was removed in SE–0029. In that case, converting between a tuple and a function’s argument list, the type is changing so it makes sense to have an explicit operator to perform the conversion. But here no such modification occurs.

4 Likes

So the desired approach is some more compiler magic? If that is the case, it would really be best not to involve any new or existing operators in this.

To everyone
Is there someone who thinks an “operator” is really necessary? If so… why?

2 Likes

I don’t remember on what list, or precisely when, but shortly before the switch to the forums, I remember having seen a mail-thread about possibly having variadics being, at the ABI level, a compiler-generated buffer type instead of an actual Array, which if extended to a source level type would provide this behavior.

(I just thought that statement should be repeated ;-)

Agreed as well — but it only solves the problem of forwarding, not the initial request of using variadic functions with arrays.
Now, T… is in my understanding nothing but an array in disguise, so a variadic function may as well accept a normal array. So… why not simply stop the masquerade, and replace T… with [Array]?

1 Like

This makes things ambiguous if you want to specialize based on the number of arguments.

How so?

Here is the current situation:

func foo()                   { print("0 args") }
func foo(_ a: Any)           { print("1 arg")  }
func foo(_ a: Any, _ b: Any) { print("2 args") }
func foo(_ abc: Any...)      { print("n args") }

func bar()                   { foo()     }
func bar(_ a: Any)           { foo(a)    }
func bar(_ a: Any, _ b: Any) { foo(a, b) }
func bar(_ abc: Any...)      { foo(abc)  }

bar()                       // prints “0 args”
bar(4)                      // prints “1 arg”
bar("hi", "world")          // prints “2 args”
bar(3, 1, 4, 1, 5)          // prints “1 arg”

I am saying the last line should print “n args”. This would change the meaning of some code, which is potentially source-breaking, but it would not be *ambiguous*.

For readers, I actually think that this isn’t immediately clear. It’s more subtle than I would like, especially once you start allowing arrays to be passed in and implicitly turned into variadic args.

Indeed, bar() is ambiguous in Nevin’s example. It can be either func bar(_ arg: Any...) or func bar() {...}. Is it intended for the function without arguments to take precedence to disambiguate? The same can be said about the other ones.

+1, we should clearly support this. On the syntactic side of things, I don’t think that …x is the right way to spell this anyway. I’d suggest thinking about a #varargsSplat(x) style syntax, since this is super important to have, but not important and prominent enough to introduce a new top level operator for it. A little verbosity here would be a good thing.

-Chris

7 Likes

I’d prefer pure compiler magic, where any variadic argument can take an array where there’s no ambiguity.

If we do have to go with explicit splattage, I’d prefer to avoid an operator and use a method if the language can be made to do that:

myArray.variadic() // [T] -> T..., or [T] -> SwiftVarArg<T>

rather than the spellings of #varargsSplat(myArray) or the other suggestions upthread.

1 Like

If the argument type is “Any...” how do you know if you are supposed to splat an array or pass it as a single element?

Pick one and document it?

Specifically, default to splatting when forwarding one variadic argument to another, and let people write “as Array” if they want to cast?

3 Likes

Did anything come of this?
I just came hunting for a way to forward variadic params and couldn't find anything.

In my case, it's the classic SwiftUI pattern of a ViewModifier with a convenience extension on View

It would be nice not to have to duplicate my initialisers...

extension View {

    func capture(name:String,id:String? = nil,tags:CustomStringConvertible...) -> some View {
        modifier(CaptureModifier(name: name,id:id,tags:tags.map {$0.description}))
    }
}

public struct CaptureModifier: ViewModifier {
    var name: String
    var tags:[String]
    var id:String?

    public init(name:String,id:String?,tags:[String]) {
        self.name = name
        self.tags = tags
        self.id = id
    }
    
    public init(name:String,id:String?,tags:CustomStringConvertible...) {
        self.name = name
        self.tags = tags.map {$0.description}
        self.id = id
    }
    
    public func body(content: Content) -> some View {
        //snip
    }
}

Sorry for citing after two years, but this behavior is still present today.
I personally think that after writing line 4, the compiler should warn with an "Invalid redeclaration of 'foo' " since the declaration on that line can cover the previous declarations (it's a generalization over the other three, if we may say).

Swift is known for explicitness and ease of understanding. Suppose that a user doesn't know that there's a function foo already in the scope, defines a func foo(_ abc: Any...) and then runs that function with one or two arguments. His function wouldn't be executed and that would be an undefined behavior in some sense, at least from my point of view.
Swift prefers the specialized declaration over the generalized one, but on the call site they're the same from the user perspective.