swifttypesclosurescontravarianceliskov-substitution-principle

How to pass the print function as argument to a forEach?


The print function can print integers and can be called like print(5). I want to do the same on an [Int] array.

However attempting to do the same on a forEach fails.

[1,2,3].forEach(print)

gives

expression failed to parse:
error: repl.swift:20:17: error: cannot convert value of type
 '(Any..., String, String) -> ()' to expected argument type 
'(Int) throws -> Void'

[1,2,3].forEach(print)
                ^

How can I get Swift to perform this conversion from (Any, ... String, String) -> () to (Int) throws -> Void without resorting to the workarounds listed below?

Aren't function types contravariant in the parameter position, meaning that since Any is a supertype of Int, (Any, ... String, String) -> () is a subtype of (Int) throws -> Void (since () is the same as Void) and therefore by the Liskov substitution principle, print should be a perfectly acceptable argument to forEach in this case?

Is it an issue with the number of (variadic AND optional!) parameters in (Any, ... String, String)?

Workarounds

These work but they seem unnecessary to me. Creating an inline closure using shorthand argument name [1,2,3].forEach { print($0) } or defining a function func printInt(_ n: Int) { print(n) }; [1,2,3].forEach(printInt) both work.

This suggests that the issue is at least partly with the number of arguments. However I am not clearly understanding what is happening here and would appreciate any help.


Solution

  • Is it an issue with the number of (variadic AND optional!) parameters in (Any, ... String, String)?

    Yes. There is no syntax for function type parameters that say "this is an optional parameter". When you treat print as a first class object, its optional parameters automatically becomes required.

    let p = print
    // p's type is (Any..., String, String) -> Void
    
    func notOptionalPrint(_ items: Any..., separator: String, terminator: String) { ... }
    let q = notOptionalPrint
    // q's type is also (Any..., String, String) -> Void
    

    Furthermore, the type of the first parameter is Any.... This is not the same as Any. It is not a supertype of Int or Any, so LSP does not apply here, even if the optional parameters problem is magically solved.

    Therefore, you have to wrap it in some way if you want to pass it directly to forEach. Rather than wrapping it just for Int, you can consider wrapping it for Any:

    func print(_ x: Any) {
        // need "Swift." here, otherwise infinite recursion
        Swift.print(x)
    }
    

    Then, because of LSP, you can pass this directly to forEach for collections of any element type.

    Personally though, I wouldn't bother, and just use forEach { print($0) }.