Are () -> () and (()) -> () supposed to be interchangeable or not?

Consider this example:

func retrieveFunc<T, U, V>( _ t: T, _ block: @ escaping (T) -> (U) -> V) -> (U) -> V {
return block(t)
}

class Some {

func stringifyInteger( _ int: Int) -> String {
return String(int)
}

func doNothing() {}
}

let some = Some()
let x = retrieveFunc(some, Some.stringifyInteger)
let y = retrieveFunc(some, Some.doNothing)

The code won't compile with an error:

Cannot convert value of type '(Some) -> () -> ()' to expected argument type '() -> () -> _'

I think it's because () -> () and (()) -> () are seen as distinct by the compiler.
But in this scenario () -> () will get promoted to (()) -> ():

func just<T, U>( _ block: @escaping (T) -> U) -> (T) -> U {
return block
}

func emptyFunc() {}

func stringifyInteger( _ int: Int) -> String {
return String(int)
}

let x = just(emptyFunc)//(()) -> ()
let y = just(stringifyInteger)

So what's going on here?

In an argument conversion context, an argument value of type (A, B...) -> () can be passed where a parameter type of ((A, B...)) -> () is declared (here I'm using A/B to denote any number of positional arguments, not a "variadic" argument type per se).

As a consequence of this rule, you're seeing () -> () converting to (()) -> (), which is just a special case with no arguments.

The rule does not apply in non-argument position, so (Some) -> () -> () does not convert to (Some) -> (()) -> (), since the function conversion is the result type of a function conversion in argument position, and the "top level" of an argument.

This came about because in Swift 2 and older, ((A, B...) -> () and (A, B...) -> () were actually the same type. All function types had a single input, which could be a tuple, and furthermore, for any type T, T and (T) were the same type. In Swift 3, a set of proposals were adopted that actually changed function types to conceptually take multiple arguments and not a single argument that may be a tuple. However the internal representation was not changed in Swift 3 because it was such a big change that touched all parts of the compiler. In Swift 4, we started adopting the constraint solver to use the correct representation, and that uncovered a number of cases where the compiler was allowing this "tuple splat" through. Because of community feedback we decided to formalize it as a language rule, but only in argument conversion position.

In Swift 5 the internal representation of function types now fully conforms to the updated understanding of the language semantics and this conversion exists as an explicitly-implemented part of the type checker.

I'm not totally happy it exists since it can create confusion, but overall the alternative of having poor ergonomics when, eg, mapping over a dictionary, was worse.

10 Likes

Thank you so much for this detailed explanation! It seems that I won’t be able to "weakify" such void taking and returning methods akin to thread.
I guess I have some reading of evolution proposals to do :)

Terms of Service

Privacy Policy

Cookie Policy