Swift: Task + DispatchTimeInterval

Improving Task.sleep for everyone.

Swift: Improving Task.sleep

Motivation

Task.sleep(nanoseconds:) is pretty awkward to use, as it necessitates use of UInt64 to specify a time with nanosecond precision. This precision is nice when you are working with very small slices of time, but it's less useful when dealing with timeouts measured in seconds.

Task.sleep

Let's fix it up to accept the highly-useful DispatchTimeInterval enum as an argument instead..

public extension Task where Success == Never, Failure == Never {
    static func sleep(_ timeInterval: DispatchTimeInterval) async throws {
        let duration: UInt64
        switch timeInterval {
        case let .seconds(s):
            duration = UInt64(s)  * 1_000_000_000
        case let .milliseconds(ms):
            duration = UInt64(ms) * 1_000_000
        case let .microseconds(us):
            duration = UInt64(us) * 1_000
        case let .nanoseconds(ns):
            duration = UInt64(ns) * 1
        case .never:
            duration = .max
        @unknown default:
            fatalError("Unknown DispatchTimeInterval case in Task.sleep(_:)")
        }
        try await Task.sleep(nanoseconds: duration)
    }
}

Note that we must constrain the extension to where Success == Never, Failure == Never in order to satisfy the requirements of Task.sleep(nanoseconds:), the documentation of which states:

Available when Success is Never and Failure is Never.

This is because Task takes two generic parameters Success and Failure, and the sleep(nanoseconds:) function is only availale for Task<Never,Never>.

Now, let's see our function being used:

print("sleeping for 1 second...")
try await Task.sleep(.seconds(1))
print("running task!")

Well, that was easy!

Task.after

We can also create a convenience method to run a task after a delay, using our new sleep function.

extension Task where Failure == Error {
    static func after(
        _ timeInterval: DispatchTimeInterval,
        priority: TaskPriority? = nil,
        operation: @escaping @Sendable () async throws -> Success
    ) -> Task {
        Task(priority: priority) {
            try await Task<Never,Never>.sleep(timeInterval)
            return try await operation()
        }
    } 
}

This time, instead of just waiting, we have an operation that we need to call after the delay.

Note that we have different constraints on the extension this time. We don't have the Never constraints, because we're only using the sleep task as a subtask, but we do have a Failure == Error constraint, because we have an operation that could potentially fail. Also note how we had to specify Task<Never,Never> in order to call sleep(_:) from this extension, because here Task refers to Task<s,Error> where the Success type s is undetermined.

Using this function is nice and easy too!

print("running task after 1 second...")
try await Task.after(.seconds(1)) {
    print("running task!")
}

No more mucking about with nanosecond conversion!