`Void` gets wrapped inside parentheses in generic types

Sample Code:

class Tester<Output, Failure: Swift.Error> {
    typealias Execute = (
        _ onSuccess: @escaping (Output) -> Void,
        _ onFailure: @escaping (Failure) -> Void
    ) -> Void

    let execute: Execute

    init(execute: @escaping Execute) {
        self.execute = execute
    }
}

class Provider {
    struct Error: Swift.Error { }

    func execute(
        onSuccess: @escaping () -> (),
        onFailure: @escaping (Error) -> ()
    ) {
        DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
            if Bool.random() {
                onSuccess()
            } else {
                onFailure(.init())
            }
        }
    }
}

let provider = Provider()
let tester: Tester<Void, Provider.Error> = .init(execute: provider.execute)

Following complier error prompts:

Cannot convert value of type '(@escaping () -> (), @escaping (Provider.Error) -> ()) -> ()' to expected argument type '(@escaping (Void) -> Void, @escaping (Provider.Error) -> Void) -> Void'

it seems the complier is expecting a (Void) a.k.a (()) for the onSuccess: part.

Is this expected or a bug? How to get it right if it is not a bug, excepting for changing onSuccess: @escaping () -> () to onSuccess: @escaping (()) -> () in Provider.execute?

Thanks!

Screenshot:
Screen Shot 2022-03-21 at 12.26.09 AM

This is expected: your type alias requires that the closure have one parameter of type Output, which you then specify to be Void, and this is not the same as having zero parameters. You will therefore need to provide a closure of type @escaping (()) -> ().

(Fortunately, closures themselves have a special rule such that you can pass as an argument a concrete closure expression which has no parameters.)

2 Likes

I agree that the nested (()) is unwieldy.

1 Like

Do you mean a special overload like the following extension needs to be added in this case? Like what Combine does for PassthroughSubject with send(Output) and send() when Output is Void? But I remember I read it somewhere that the (()) to () is somewhat automatically supported in Swift?

extension Tester where Output == Void {
    typealias VoidOutputExecute = (
        _ onSuccess: @escaping () -> Void,
        _ onFailure: @escaping (Failure) -> Void
    ) -> Void

    convenience init(execute: @escaping VoidOutputExecute) {
        self.init { onSuccess, onFailure in
            execute {
                onSuccess(())
            } _: {
                onFailure($0)
            }
        }
    }
}

This is not true. A function/closure with zero arguments is not the same as, nor can it be converted to, a function/closure with a single Void argument.

1 Like

I can for closures. And, in fact, it does.

You can what?

What does it refer to and what does it do?

The "I" was a typo, and should have been "It".
And those "it"s refer to the "it" I quoted from you.

You stated:

A function/closure with zero arguments is not the same as, nor can it be converted to, a function/closure with a single Void argument.

I claim, it can. And in fact, it does.

@sveinhal Give me an example.

I did, in the link above. But I can inline an example for your convenience :wink:

import Combine

let subject = PassthroughSubject<Void, Never>()

func eventHandler() {
    print("event triggered")
}

subject.sink(receiveValue: eventHandler)
subject.send(())

Here eventHandler is a function with zero arguments, which is converted to a closure with a single Void argument. It this wasn't the case, you'd have to write it like this:

func eventHandler(_ value: Void) {
    print("event triggered \(value)")
}

This is super useful, especially when passing closure literals, and not having to write _ in

subject.sink { _ in // <- not needed, even though it expects a `(Void) -> ()` closure
    print("event triggered")
}
2 Likes