Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does typecasting/polymorphism work with this nested, closure type in Swift?

I know that (Int) -> Void can't be typecasted to (Any) -> Void:

let intHandler: (Int) -> Void = { i in
    print(i)
}
var anyHandler: (Any) -> Void = intHandler <<<< ERROR

This gives:

error: cannot convert value of type '(Int) -> Void' to specified type '(Any) -> Void'


Question: But I don't know why this work?

let intResolver: ((Int) -> Void) -> Void = { f in
    f(5)
}

let stringResolver: ((String) -> Void) -> Void = { f in
    f("wth")
}

var anyResolver: ((Any) -> Void) -> Void = intResolver

I messed around with the return type and it still works...:

let intResolver: ((Int) -> Void) -> String = { f in
    f(5)
    return "I want to return some string here."
}

let stringResolver: ((String) -> Void) -> Void = { f in
    f("wth")
}

var anyResolver: ((Any) -> Void) -> Any = intResolver (or stringResolver)

Sorry if this is asked before. I couldn't find this kind of question yet, maybe I don't know the keyword here. Please enlighten me!

If you want to try: https://iswift.org/playground?wZgwi3&v=3

like image 277
aunnnn Avatar asked Feb 18 '18 05:02

aunnnn


Video Answer


2 Answers

It's all about variance and Swift closures.

Swift is covariant in respect to closure return type, and contra-variant in respect to its arguments. This makes closures having the same return type or a more specific one, and same arguments or less specific, to be compatible.

Thus (Arg1) -> Res1 can be assigned to (Arg2) -> Res2 if Res1: Res2 and Arg2: Arg1.

To express this, let's tweak a little bit the first closure:

import Foundation

let nsErrorHandler: (CustomStringConvertible) -> NSError = { _ in
    return NSError(domain: "", code: 0, userInfo: nil)
}
var anyHandler: (Int) -> Error = nsErrorHandler

The above code works because Int conforms to CustomStringConvertible, while NSError conforms to Error. Any would've also work instead of Error as it's even more generic.

Now that we established that, let's see what happens in your two blocks of code.

The first block tries to assign a more specific argument closure to a less specific one, and this doesn't follow the variance rules, thus it doesn't compile.

How about the second block of code? We are in a similar scenario as in the first block: closures with one argument.

  • we know that String, or Void, is more specific that Any, so we can use it as return value
  • (Int) -> Void is more specific than (Any) -> Void (closure variance rules), so we can use it as argument

The closure variance is respected, thus intResolver and stringResolver are a compatible match for anyResolver. This sounds a little bit counter-intuitive, but still the compile rules are followed, and this allows the assignment.

Things complicate however if we want to use closures as generic arguments, the variance rules no longer apply, and this due to the fact that Swift generics (with few exceptions) are invariant in respect to their type: MyGenericType<B> can't be assigned to MyGenericType<A> even if B: A. The exceptions are standard library structs, like Optional and Array.

like image 104
Cristik Avatar answered Sep 28 '22 09:09

Cristik


First, let's consider exactly why your first example is illegal:

let intHandler: (Int) -> Void = { i in
    print(i)
}
var anyHandler: (Any) -> Void = intHandler
// error: Cannot convert value of type '(Int) -> Void' to specified type '(Any) -> Void'

An (Any) -> Void is a function that can deal with any input; an (Int) -> Void is a function that can only deal with Int input. Therefore it follows that we cannot treat an Int-taking function as a function that can deal with anything, because it can't. What if we called anyHandler with a String?

What about the other way around? This is legal:

let anyHandler: (Any) -> Void = { i in
  print(i)
}
var intHandler: (Int) -> Void = anyHandler

Why? Because we can treat a function that deals with anything as a function that can deal with Int, because if it can deal with anything, by definition it must be able to deal with Int.

So we've established that we can treat an (Any) -> Void as an (Int) -> Void. Let's look at your second example:

let intResolver: ((Int) -> Void) -> Void = { f in
    f(5)
}

var anyResolver: ((Any) -> Void) -> Void = intResolver

Why can we treat a ((Int) -> Void) -> Void as an ((Any) -> Void) -> Void? In other words, why when calling anyResolver can we forward an (Any) -> Void argument onto an (Int) -> Void parameter? Well, as we've already found out, we can treat an (Any) -> Void as an (Int) -> Void, thus it's legal.

The same logic applies for your example with ((String) -> Void) -> Void:

let stringResolver: ((String) -> Void) -> Void = { f in
  f("wth")
}

var anyResolver: ((Any) -> Void) -> Void = stringResolver

When calling anyResolver, we can pass an (Any) -> Void to it, which then gets passed onto stringResolver which takes a (String) -> Void. And a function that can deal with anything is also a function that deals with strings, thus it's legal.

Playing about with the return types works:

let intResolver: ((Int) -> Void) -> String = { f in
  f(5)
  return "I want to return some string here."
}

var anyResolver: ((Any) -> Void) -> Any = intResolver

Because intResolver says it returns a String, and anyResolver says it returns Any; well a string is Any, so it's legal.

like image 30
Hamish Avatar answered Sep 28 '22 10:09

Hamish