Using tuple pattern in closure parameters

Take the following code as an example:

extension Array {
    mutating func indices(_ condition: (Element)->Bool) -> [Index] {
        enumerated().compactMap { tuple in
            let (index, element) = tuple
            return condition(element) ? index : nil
        }
    }
}

I find it can be made more concise by moving tuple pattern to closure's parameters:

extension Array {
    mutating func indices(_ condition: (Element)->Bool) -> [Index] {
        enumerated().compactMap { (index, element) in  // !!!
            condition(element) ? index : nil
        }
    }
}

I'd like to use this feature, but I'm not sure if it's by design or by accident. In the closure expression section of Swift language reference, it says:

The parameters have the same form as the parameters in a function declaration, as described in Function Declaration.

But I don't think function has this feature becuase function can't be defined inline and hence compiler doesn't have the context to validate the tuple pattern. Actually I'm not sure if (index, element) in above code is a tuple pattern, because it doens't work in general. See example below:

func f(c: ((Int, Int), Int) -> Void) {
    c((0,0), 0)
}

func test() {
    // This doesn't compile. Error: Cannot find 'x' in scope
    f { (x, y), z in
    }
}

It it really a tuple pattern, then I have a further question. Are index and element parameters or local variables? I think calling them parameters seems odd because the closure takes one parameter only. So I'm currently understanding them as (immutable) local variables.

I wonder how people in the forum think about it. Is it OK to use it? Thanks.

3 Likes

I’ve used it before to refer to the components of the tuple as $0 and $1 rather than $0.0 and $0.1, but both are valid. Arguably this is a holdover from early Swift’s implicit tuple splat, but this one case was left in the language, so might as well use it.

1 Like

Actually, looking through my old code, I found one very clever use of this: elementwise subtraction of two sequences by zip(foo, bar).map(-).

Thanks. I asked a question about tuple splatting a while back. I think below is an example of tuple splatting:

func f(_ c: ((Int, Int)) -> Void) {
    let data = (1, 2)
    c(data)
}

func test1() {
    f { x, y in
        print("x: \(x), y: \(y)")
    }
}

But the example in my current question is different. It essentially does the following:

func f(_ c: ((Int, Int)) -> Void) {
    let data = (1, 2)
    c(data)
}

func test2() {
    f { (x, y) in  // Note it's "(x, y)", not "x, y"!
        print("x: \(x), y: \(y)")
    }
}

It's not obvious to me why this is tuple splatting, unless (x, y) is equivalent to x, y in above code. I for one don't think they are equivalent. That's why I thought it's tuple pattern.

It is fine to use, because it is a deliberate, special-case exception to the rules implemented in SE-0110:

2 Likes

It is. You can say (x: Int, y: Int), or (x, y), or x, y, but not x: Int, y: Int without parentheses.

1 Like

Interesting find. Looks like there's special treatment of tuple destructuring in one specific case when there is a single tuple parameter but not in other cases:

func e(c: ((Int, Int)) -> Void) {}
func f(c: ((Int, Int), Int) -> Void) {}
func g(c: ((Int, Int), (Int, Int)) -> Void) {}

func test() {
    e { x in }           // βœ…
    e { (x) in }         // βœ…
    e { x, y in }        // βœ…
    e { (x, y) in }      // βœ…
    f { x, z in }        // βœ…
    f { (x, z) in }      // βœ…
    f { ((x, y), z) in } // πŸ›‘ Error: Closure tuple parameter does not support destructuring
    g { x, z in }        // βœ…
    g { (x, z) in }      // βœ…
    g { ((x, y), (z, w)) in } // πŸ›‘ Error: Closure tuple parameter does not support destructuring
}
2 Likes

Thank all for the information. It all makes sense to me now.

Thanks. I didn't read that thread carefully. It actually had an example same as the one I asked about.

You're right. I somehow got used to the x,y form and didn't realize closure's type declaration is same as that of function and requires parenthesis by default. The x,y is a simplified form instead. Below is in the official doc (emphasis mine).

Because all of the types can be inferred, the return arrow (->) and the parentheses around the names of the parameters can also be omitted

The way I understand it is that tuple splatting is just one level. That makes sense since the feature is for convenience only. The nested tuple is considered as tuple pattern, which is not allowed. I have an explanation why it isn't supported - ((x, y), z) is invalid function type declaration.

You may find it's interesting to read SE-0029 too. It explained why tuple splatting was introduced in the first place.

From a historical perspective, the tuple splat form of function application dates back to very early Swift design (probably introduced in 2010, but possibly 2011) where all function application was of a single value to a function type. For a large number of reasons (including inout, default arguments, variadic arguments, labels, etc) we completely abandoned this model, but never came back to reevaluating the tuple splat behavior.

BTW, I find this blog post of jrose has a lot of helpful information on tuple splatting.

1 Like