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 :)