Why Void? closure can't be passed to Void closure, inconsistently?

There is inconsistent compiler behavior when passing a () -> Void? block to a parameter that takes () -> Void:

func doThing(completion: () -> Void) {}

enum Response {
    case success
    case failure
}

var onResponse: ((Response) -> Void)?


let onSuccess = {
    onResponse?(.success)
}

// ERROR: Cannot convert value of type '() -> Void?' to expected argument type '() -> Void'
doThing(completion: onSuccess)

// inlining the closure succeeds
doThing(completion: {
    onResponse?(.success)}
)

There should be no difference via type inference between inlining the closure and using an intermediate variable, yet only inlining succeeds.

let onSuccess has an implicit type of () -> (Void?), vs when it’s inlined the compiler can look at the function signature and deduce that it’s () -> Void.

1 Like

can you explain what it's deducing? the type of the closure returns Void? in both cases. If the compiler can deduce that we can make a Void? closure into Void when inline, shouldn't it accept all Void? closures for Void

This version:

// inlining the closure succeeds
doThing(completion: {
    onResponse?(.success)}
)

benefits from the type context provided by the completion parameter. The closure can be type-checked as () -> Void by ignoring the return value. But in this version:

let onSuccess = {
    onResponse?(.success)
}

There's no context to infer the () -> Void type for the closure, so the compiler is treating it as a single-expression closure with a return value, and analyzes the type of the onResponse?(.success) expression to pick the return type.

Of course, inferring a type of () -> Void? here is pretty silly. We probably ought to warn that this is happening and ask for an explicit type annotation or return statement to clarify your intent. You can resolve this either by declaring let onSuccess: () -> Void or having a return appear after the onResponse call.

5 Likes

The behavior is a manifestation of the single-expression closure. The Swift Programming Language: Closures: Implicit Returns from Single-Expression Closures says:

Single-expression closures can implicitly return the result of their single expression by omitting the return keyword from their declaration.

That is what is dictating the inferred return type of onSuccess closure.

So, consider your:

You can fix this with either:

let onSuccess = {
    _ = onResponse?(.success)
}

Or

let onSuccess = {
    onResponse?(.success)
    return
}

Or

let onSuccess: () -> Void = {
    onResponse?(.success)
}

Each of those solve the unintended inference of the single-expression closure.

1 Like

No, it is different. I rewrite your with type annotations to make it clear.

func doThing(completion: () -> Void) {}

enum Response {
    case success
    case failure
}

var onResponse: ((Response) -> Void)? // here the closure itself is optional, it can be nil. 
// So it is effectively an
var onResponse: Optional<((Response) -> Void)> // usage:
//  if let closure = onResponse {
//    closure(.success) // can call it here
//  } else {
//    // no closure, nothing to call
//  }




let onSuccess: () -> Optional<Void> = {
    onResponse?(.success)
} // here we have a non-optional closure, we can call it without if-let dance.
// But this closure can return either Void or nil

// So:
// 1) onResponse is an Optional-type instance containing either closure (or function) or nil
// 2) onSuccess is a function that return optional value (Optional<Void> value)

// ERROR: Cannot convert value of type '() -> Void?' to expected argument type '() -> Void'
doThing(completion: onSuccess) // error as expected
// completion is of type `() -> Void` but onSuccess is of type ` () -> Optional<Void>`

// inlining the closure succeeds 
doThing(completion: { // – as expected, because `() -> Void` closure type is inferred from `doThing(completion:)` function
    onResponse?(.success)}
)

You can rewrite `onSuccess` like this:

let onSuccess: () -> Void = { // explicit function type
  onResponse?(.success)
}

let onSuccess = {
  onResponse?(.success)
  return // explicit return
}