SE-0279 and variadic parameters

I'm currently looking into whether my proposal for multiple variadic parameters in functions will need any updates now that SE-0279 is accepted. While experimenting with the implementation on master, I noticed the following is not allowed:

func foo(bar: ()->(), baz: ()->()..., qux: ()->()) {}
foo {} baz: {} _: {} _: {} qux: {} // error: extra arguments at positions #4, #3, #3, #4 in call

I assume this is mostly a consequence of the backwards scan argument matching rule, but is it the intended behavior (the error message indicates it may just be a bug)? The proposal text doesn't mention varargs. I think choosing to allow it or disallow it would both be reasonable, but it would be nice to specify the intended behavior a little more clearly.

4 Likes

As far as I can recall, variadic parameters weren't raised in the discussion or review of multiple trailing closures. I think the spelling you propose here is probably a reasonable analog to non-trailing-closure variadic params, representing them as additional unlabeled parameters. The backwards-scan rule from the proposal definitely seems incompatible with variadic parameters, though. The backwards scan in your example will match qux first, then attempt to match the (second) _ parameter, for which this check in the implementation, from what I can tell, will fail for all parameters. Same goes for argument #3 (also '_'), and from there on, backwards-scan matching finds the appropriate parameters for the baz and unlabeled arguments.

A rule which allows for variadic trailing closures would have to complicate this scan even more :pensive:. I think the procedure would basically have to be:

  1. Backwards scan through the arguments, starting from the last argument.
  2. Upon transition from a labeled argument to an unlabeled argument (or, if the last argument is unlabeled), skip backwards through unlabeled arguments and find the first unlabeled argument in the sequence of unlabeled arguments. Gather all these unlabeled arguments as a potential variadic pack.
  3. Generate another potential variadic pack (#2) which extends the first variadic pack (#1) by prepending the (labeled) closure immediately preceding pack #1.
  4. Backwards scan through the parameters looking for a match for either pack (preferring a match for pack #1).
  5. If such a match is found, match the argument pack with the variadic param, and proceed from step 1 beginning at the last argument not part of the matched pack, and the last parameter not matched with the variadic param.
  6. If no match is found, return back to the argument index from (2) where the potential variadic pack ended, and proceed with the normal backwards-scan matching rule for multiple trailing closures.

This feels... extremely messy, but maybe in "sane" cases it yields intuitive results. It's certainly a difficult rule to explain to anyone trying to learn about trailing closures. Unless someone can radically simplify the rule (maybe I'm overcomplicating things!), I think I come down on the side of simply prohibiting variadic parameters interacting with multiple trailing closures, for the time being.

OTOH, the following compiles fine today, which suggests that variadic arguments in trailing closure position "should" extend to multiple trailing closures...

func test(fns: (() -> Void)...) {}

test {}

Maybe it would be too surprising to say "yes, you can pass a single closure to a variadic parameter, but not multiple closures"?

1 Like

I agree this is surprising, but maybe it's the best course of action, for Swift 5.3 at least. While I think the backwards scan could be adapted as you describe, it seems like it would add an unfortunate amount of complexity. Also, users always have the fallback of not using trailing closures.

In practice, I can't think of a reason someone would write a function like the one from my original example, so I don't think such a limitation would be a major flaw. Maybe we just need to come up with a way to communicate how the backwards scan works to the user when they make a mistake?

1 Like

Just curious, and don't have a toolchain handy to test—does foo {} baz: {} qux: {} compile on master? If so, I think maintaining that behavior for 5.3 is probably the best call, and maybe improving the diagnostic to say "cannot (yet?) pass variadic arguments in trailing closure position" rather than the current nonsense. I agree that it's probably unlikely to arise in practice.

As to whether that limitation should be considered a bug in the implementation of SE-0279, or would require a further evolution proposal, that's something for the Core Team to answer. It's probably worth filing a bug for the time being just to track this issue!

1 Like

It looks like this does indeed compile on a near-master build, so the current implementation is consistent, even if the limitation is a little odd. I'll put together a bug report, but I think for now trailing closures at least doesn't complicate allowing multiple variadic params in a function.

1 Like