Ray Toal
Ray Toal

Reputation: 88378

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.

Upvotes: 3

Views: 432

Answers (1)

nRewik
nRewik

Reputation: 9148

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?

Upvotes: 3

Related Questions