Ray Toal Ray Toal - 1 month ago 12
Swift Question

Why don't shorthand argument names work in this Swift closure?

Here's a Swift function that takes in two ints and a three-arg function, and calls the passed-in function.

func p(x:Int, _ y:Int, _ f: (Int, Int, Int) -> ()) {
f(x, y, 0)
}


I can call this just fine using both trailing closure syntax and shorthand argument names, no problem:

> p(1, 2) {print($0 + $1 + $2)}

3


That worked as expected. But in the Foundation library, there is a string method called
enumerateSubstringsInRange
defined as follows:

func enumerateSubstringsInRange(
_ range: Range<Index>,
options opts: NSStringEnumerationOptions,
_ body: (substring: String?,
substringRange: Range<Index>,
enclosingRange: Range<Index>,
inout Bool) -> ())


Okay, that's easy enough: the function takes three arguments, the last of which is four-argument function. Just like my first example! Or so I thought....

I can use this function with the trailing closure syntax, but I cannot use shorthand argument names! I have no idea why. This is what I tried:

let s = "a b c"
"a b c".enumerateSubstringsInRange(s.characters.indices, options: .ByWords) {(w,_,_,_) in print(w!)}

a
b
c


All good; I just wanted to print out the matched words, one at a time. That worked when I specified by closure as ``{(w,,,_) in print(w!)}`. HOWEVER, when I try to write the closure with shorthand argument syntax, disaster:

> "a b c".enumerateSubstringsInRange(s.characters.indices, options: .ByWords) {print($0!)}
repl.swift:9:86: error: cannot force unwrap value of non-optional type '(substring: String?, substringRange: Range<Index>, enclosingRange: Range<Index>, inout Bool)' (aka '(substring: Optional<String>, substringRange: Range<String.CharacterView.Index>, enclosingRange: Range<String.CharacterView.Index>, inout Bool)')


So what did I do wrong?! The error message seems to say that closure argument
$0
is the whole tuple of args. And indeed, when I tried that, that sure seems to be the case!

>"a b c".enumerateSubstringsInRange(s.characters.indices, options: .ByWords) {print($0.0!)}

a
b
c


So I'm terribly confused. Why in the first case (my function
p
, are the arguments understood to be
$0
,
$1
, etc., but in the second case, all the arguments are rolled up into a tuple? Or are they? FWIW, I found the signature of enumerateSubstringsInRange here.

Answer

It depends on the number of parameters.

For example,

func test( closure: (Int,Int,Int) -> Void ){
    // do something
}

To make test works as you expect, you must specify $2 ( 3rd argument ). The compiler will infer to the values inside tuple, otherwise it will infer to the tuple itself.

If you don't specify $number that match the number of parameters. For example, only specify $1, will make compile error.

// work as expected ( infer to int )
test{
    print($2)
}
test{
    print($1+$2)
}
test{
    print($0+$1+$2)
}

// not work ( infer to tuple )
test{
    print($0)
}

// not work ( cannot infer and compile error )
test{
    print($1)
}

There is a question relate to this question. Why is the shorthand argument name $0 returning a tuple of all parameters?

Comments