I've noticed that I can cast a closure that has regular arguments to a closure that has its arguments wrapped in a tuple. But only if I use a particular method of casting!
let myClosure = { (a: Int, b: Float) -> Void in
print(a, b)
}
// I want to convert the closure to be of this type.
var myClosureWithTupleArgVar: (((Int, Float)) -> Void)? = nil
// Cast A is possible.
myClosureWithTupleArgVar = (((Int, Float)) -> Void)?(myClosure)
myClosureWithTupleArgVar?((1, 2))
// Cast B will always fail and return nil (as warned by the compiler).
myClosureWithTupleArgVar = myClosure as? (((Int, Float)) -> Void)
myClosureWithTupleArgVar?((3, 4))
Outputs:
1 2.0
Why is it possible to cast using cast A but not cast B? What is the difference between using an as
Swift-style cast and the C-style function call cast?
(I am not interested in the difference between as, as?, and as!)
The extra parenthesis is reason why facing this warning (despite this, it shows the result of " 1 2.0 3 4.0" on playground).
First, let me confirm that:
it should be myClosureWithTupleArgVar?((1, 2))
instead of myClosureWithTupleArgVar?(1, 2)
as well as myClosureWithTupleArgVar?((3, 4))
instead of myClosureWithTupleArgVar?(3, 4)
That's because myClosureWithTupleArgVar
type is (((Int, Float)) -> Void)?
.
For a reason, the compiler recognizes that (Int, Float) -> Void
(the type of myClosure
) is not the same as ((Int, Float)) -> Void
(the type of myClosureWithTupleArgVar
). At this point, if you tried to edit your code as:
let myClosure = { (a: Int, b: Float) -> Void in
print(a, b)
}
var myClosureWithTupleArgVar: ((Int, Float) -> Void)? = nil
// This cast is possible.
myClosureWithTupleArgVar = ((Int, Float) -> Void)?(myClosure)
myClosureWithTupleArgVar?(1, 2)
myClosureWithTupleArgVar = myClosure as? ((Int, Float) -> Void)
myClosureWithTupleArgVar?(3, 4)
by removing the extra parenthesis (((Int, Float) -> Void)?
instead of (((Int, Float)) -> Void)
) you should see the opposite warning! Which is:
Conditional cast from '(Int, Float) -> Void' to '(Int, Float) -> Void' always succeeds
which means that you don't even have to mention the as
casting anymore (they are having the exact same type for now):
myClosureWithTupleArgVar = myClosure
instead of
myClosureWithTupleArgVar = myClosure as? ((Int, Float) -> Void)
Also:
myClosureWithTupleArgVar = myClosure
instead of:
myClosureWithTupleArgVar = ((Int, Float) -> Void)?(myClosure)
Keep in mind that this case is not only for casting closures. Example:
let int1 = 100
var int2: Int? = nil
// unnecessary castings:
int2 = Int(int1) // nothing shown here, because of Int init: init(_ value: Int)
int2 = int1 as? Int // Conditional cast from 'Int' to 'Int' always succeeds
The C-Style cast basically means the Swift compiler will just force your closure to be called as if it takes a (Int, Float)
tuple as parameter whereas the as / as? / as! cast will first do some sanity checks on your cast to ensure that the types are compatible and so on.
Since the compiler believes (in certains versions, as seen on the comments on the other answer) that (Int, Float) -> ()
and ((Int, Float)) -> ()
are too far apart to be compatible, the sanity check will just return nil, therefore blocking your call.
What makes it work is that a function / closure taking a (Int, Float)
tuple behaves exactly the same (in the current version of Swift) as a function / closure taking an Int
and a Float
parameter.
I compiled a snippet of code into assembly which I will be referencing from now on. That snippet can be found here : https://swift.godbolt.org/z/CaOb0s
For readability purposes, I used functions instead of actual closures here.
I've created two functions corresponding to the two cases we have :
func twoParamFunc(a: Int, b: Float)-> Void {
print(a, b)
}
func singleParamFunc(tuple: (a: Int, b: Float))-> Void {
print(tuple.a, tuple.b)
}
I then tried to cast those using your two different methods :
let cCastFunction = ((((Int, Float)) -> Void)?(twoParamFunc))!
let asCastFunction = (twoParamFunc as? (((Int, Float)) -> Void))!
And when looking at the assembly code compiled by swift, we can see a lot of differences between the two.
When looking at the C-style cast, we can see that most of the code is basically just calling alloc/retain/release and moving pointers and values around. The only call to external code is through a failure case (the !
dereferencing a null reference), calling $ss18_fatalErrorMessage__4file4line5flagss5NeverOs12StaticStringV_A2HSus6UInt32VtF
Whereas in the swift-style cast, there are a lot of additional calls (the sanity checks I was talking about earlier). We have for exemple
call (type metadata accessor for (Swift.Int, Swift.Float) -> ())
...
call (type metadata accessor for ((Swift.Int, Swift.Float)) -> ())
...
call swift_dynamicCast@PLT
which clearly shows that the Swift compiler is doing some checks to the compatibility of the types being cast, and are nowhere to be found in the c-style cast.
So now that the C-style cast / Swift-style cast difference has been found, we can try to understand why the call to the C-style casted function works.
When looking at the assembly code generated by the two simple calls to the functions I made in the sample :
twoParamFunc(a: a.0,b: a.1)
singleParamFunc(tuple: a)
We can see that those functions are actually compiled to be called identically :
singleParamFunc
:
mov rdi, qword ptr [rip + (output.a : (Swift.Int, Swift.Float))]
movss xmm0, dword ptr [rip + (output.a : (Swift.Int, Swift.Float))+8]
call (output.singleParamFunc(tuple: (a: Swift.Int, b: Swift.Float)) -> ())
Here we see that the value corresponding to the first value of the tuple is put into register rdi
, and the second one is put into xmm0
, and then the function is called
twoParamFunc
:
mov rax, qword ptr [rip + (output.a : (Swift.Int, Swift.Float))]
movss xmm0, dword ptr [rip + (output.a : (Swift.Int, Swift.Float))+8]
...
mov rdi, rax
...
call (output.twoParamFunc(a: Swift.Int, b: Swift.Float) -> ())
In this function, it is not as straightforward, but now value 1 goes in rax
register which itself is copied into rdi
register, and value 2 still goes in xmm0
, and the function is called.
But in this sample since we are doing other things, the assembly code is a bit messier, I've made another sample to test this cleanly : https://swift.godbolt.org/z/vDCZZV
In this sample (on which I've added another test with a struct) we can see that the assembly code created to called the 3 functions are exactly the same :
mov rdi, qword ptr [rip + (output.structValue : output.struct_test)]
movss xmm0, dword ptr [rip + (output.structValue : output.struct_test)+8]
call (output.test(value: output.struct_test) -> ())
mov rdi, qword ptr [rip + (output.tupleValue : (Swift.Int, Swift.Float))]
movss xmm0, dword ptr [rip + (output.tupleValue : (Swift.Int, Swift.Float))+8]
call (output.test2(tuple: (Swift.Int, Swift.Float)) -> ())
mov ecx, 1
mov edi, ecx
movss xmm0, dword ptr [rip + .LCPI0_0]
call (output.test3(a: Swift.Int, b: Swift.Float) -> ())
To resume, in the current version of swift, any of these three functions could be c-casted into any other and still work.
This ended up being a lot longer than initially planned, but I thought this problem deserved it.
First of all, it's worth noting that:
myClosureWithTupleArgVar = (((Int, Float)) -> Void)?(myClosure)
is not a C-style cast, as casting in Swift is done with the as
/as?
/as!
operators.
Rather, this is syntactic sugar for:
myClosureWithTupleArgVar = Optional<((Int, Float)) -> Void>(myClosure)
which is a call to Optional
's initialiser init(_ some: Wrapped)
.
The reason why a (Int, Float) -> Void
function can be converted to a ((Int, Float)) -> Void
function through Optional
's initialiser is because Swift currently implements a special argument conversion that can transform a function with N parameters to a function that takes a single N-element tuple parameter.
For example:
typealias TupleTakingFn = ((Int, Float)) -> Void
func giveMeATupleTakingFn(_ fn: @escaping TupleTakingFn) -> TupleTakingFn {
return fn
}
let myClosure = { (a: Int, b: Float) -> Void in
print(a, b)
}
let myClosureWithTupleArgVar = giveMeATupleTakingFn(myClosure)
This conversion is only done as a special case of an argument conversion, which can only be taken when passing an argument to a parameter. It therefore cannot be used in other cases such as direct assignment or type-casting through as
/as
/as!
, which is why your cast fails.
Okay, but why does this special conversion exist? Well, in Swift 3 you used to be able to freely convert an N-arity function to an N-element tuple taking function (and vice versa), allowing the following to compile:
func foo(_ x: Int, _ y: Int) {}
func bar(_ x: (Int, Int)) {}
let fn1: (Int, Int) -> Void = bar // ✅
let fn2: ((Int, Int)) -> Void = foo // ✅
This was a remnant of the tuple splatting behaviour removed by SE-0029, and was therefore removed in Swift 4 by SE-0110, meaning that an N-arity function can now only be used where an N-arity function is expected:
let fn1: (Int, Int) -> Void = bar // ❌
let fn2: ((Int, Int)) -> Void = foo // ❌
However, this change caused a usability regression, where the following example (amongst others) would no longer compile:
let dict = [5: ""]
let result = dict.map { key, value in
(key + 1, value + "hello")
}
This is due to the fact that we're attempting to pass a closure with two parameters to the method map(_:)
which expects a function of type (Element) -> T
, which in the case of Dictionary
is ((key: Key, value: Value)) -> T
.
To address this regression and keep the above code legal, the conversion from an N-arity function to an N-element tuple taking function was re-introduced, but only a special case for argument conversions:
[W]e will “back out” the SE-0110 change regarding function arguments from Swift 4.
Specifically, when passing an argument value of function type (including closures) to a parameter of function type, a multi-parameter argument function can be passed to a parameter whose function type accepts a single tuple (whose tuple elements match the parameter types of the argument function).
This therefore allows the above example to remain legal while banning such function conversions in other places. However it also has the unfortunate side-effect of allowing your example of:
myClosureWithTupleArgVar = (((Int, Float)) -> Void)?(myClosure)
to compile.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With