anpol / DispatchKit

[abandoned] An idiomatic Swift wrapper for the Grand Central Dispatch (GCD) Framework

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Failable Initializers

ElvishJerricco opened this issue · comments

As mentioned here, it would be best to use failable initializers instead of optional dispatch objects. However, there are a few methods / initializers which (especially in DispatchData.swift) where it is not clear whether the method / initializer should fail, or return an optional or an implicitly unwrapped optional. The goal of this issue is to determine how each of these methods / initializers should behave.

Actually, I'm not sure that failable initializers is the best approach. I suspect most users are expecting that most GCD calls succeed, so they will use unwrapped values without checking for nil.

Probably, it would be better to provide an error handler that would be invoked when libdispatch call returns NULL. Such error handler could be set by the user, its possible behavior could be either assert/fail (this is the default), or log message, or ignore error completely. If the error is ignored, than we can fallback to failable initializer when feasible, or use non-failable initializer otherwise.

With an error handler, failable initializers can be safely defined as "init!" which would preserve compatibility and likely annoy users as little as possible.

While I agree that implicitly unwrapped failable initializers would be the right choice, I'm not sure if an error handler is the way to go about it. What would the error handler really do?

Default error handler could call assertionFailure() or preconditionFailure(). Another error handler could do nothing and allow control flow to proceed to failable initializer.

One possible way to illustrate this:

typealias ErrorHandler = (String, StaticString, UInt) -> Void
var errorHandler: ErrorHandler = defaultErrorHandler

func defaultErrorHandler(message: String, file: StaticString, line: UInt) {
    preconditionFailure(message, file: file, line: line)
}

func invokeErrorHandler(message: String, file: StaticString = __FILE__, line: UInt = __LINE__) {
    errorHandler(message, file, line)
}

func dispatch_api_example() -> Int? { return nil }

struct Wrapper {
    let value: Int

    init!() {
        if let result = dispatch_api_example() {
            self.value = result
        } else {
            invokeErrorHandler("it happens")
            return nil
        }
    }
}

Alright that makes sense. I'd imagine we'd want the error handler to be a constructor parameter that defaults to the default error handler?

No, I expect the global variable errorHandler, or even the static variable within the Dispatch struct.

More important is that I'm not sure we need failable initializers once we have error handler. I suspect it would be better to avoid failable initializers at all and stick to error handler mechanism.

I see a HUGE problem with that approach. If module A depends on B, and both A and B make use of DispatchKit, then only one of them can have an error handler.

Moreover, I'm not sure it makes sense that every usage of these methods should require the use of the same error handler. Shouldn't different scenarios call for different handling?

Also, I'm just not seeing why the error handler is necessary at all. If we want to generate some kind of runtime error, we can throw an ErrorType of some kind. This can be caught, handled case-by-case, and provides any necessary details to the catcher.

Finally, I don't see why any of this is better than just having a failable initializer. It'd be different if we were able to provide detailed error information. But if all we're doing is artificially producing an error and calling a handler or throwing or whatever, how is that different than a failable initializer? If the user wants to use some global handler, they can just surround these methods with their own

guard let x = DispatchType(args...) else {
    errorHandler()
}

Overall, it makes most sense to me to forego error handlers entirely in favor of idiomatic swift, with optionals.

OK then, every initializer that wraps GCD API should be defined as init!, what's special about DispatchData initializers?

BTW, what do you think about eliminating duplicate data members like DispatchQueue.queue in favor of DispatchQueue.rawValue?

I just mentioned DispatchData because it has a lot of different initializers and methods which call underlying dispatch methods.

As for eliminating duplicates, I could be wrong, but I'm fairly sure that types that implement a protocol can't change the type of a computed property to a subtype.

Although I supposed it could be genericized.

protocol DispatchObject {
    typealias RawType: dispatch_object_t
    public var rawValue: RawValue

    ...
}

It seems promising. Then we are to deprecate init(raw:) too.

The summary of changes proposed so far:

protocol _DispatchObject {
    typealias RawValue : dispatch_object_t
    var rawValue : RawValue { get }
}

struct _DispatchQueue : _DispatchObject {
    @available(*, unavailable, renamed="rawValue")
    var queue: dispatch_queue_t { return rawValue }

    @available(*, unavailable, renamed="_DispatchQueue(rawValue:")
    init!(raw value: dispatch_queue_t) { return nil }

    let rawValue: dispatch_queue_t

    init(rawValue: dispatch_queue_t) {
        self.rawValue = rawValue
    }

    init!() {
        if let queue = dispatch_queue_create(nil, nil) {
            self.rawValue = queue
        } else {
            return nil
        }
    }
}

Looks good to me. And of course introducing the same change to the other Dispatch types.

Do you want me to work on a pull request for this? Or have you already started?

Actually, I will not be able to actively maintain this until Dec 22, at least.
I would be happy if you'll get this done.

No problem. I'll get right on it.