The DispatchQueue.sync
method has this nice property that it automatically returns whatever value you return from the dispatched block. This allows you to write something like the following:
let v = queue.sync {
return ... // computation that has to be run on queue
}
This is much more elegant than having to declare an optional result variable before the call to sync
, capturing and assigning to the variable inside the closure, and finally force-unwrapping the value after sync
has returned.
NSManagedObjectContext.performAndWait(_:)
I’d like to have the same convenience for Core Data, where NSManagedObjectContext.performAndWait(_:)
plays a similar role as DispatchQueue.sync
. Core Data doesn’t ship with a suitable overload of performAndWait
, but we can provide our own in an extension:
extension NSManagedObjectContext {
func performAndWait<T>(_ block: () -> T) -> T {
var result: T? = nil
// Call the framework version
performAndWait {
result = block()
}
return result!
}
}
I chose to keep the original name, but in contrast to the existing performAndWait
the new method is generic over a type parameter T
— the return type of the block
argument and the method itself. The implementation wraps a call to the framework version of the method with the boilerplate to handle the return value.
In my limited testing, the type checker had no trouble disambiguating between the two performAndWait
overloads — it automatically chose the correct overload based on the expected return types. If you are worried about ambiguity errors, feel free to give the new method a different name.
Error handling with rethrows
DispatchQueue.sync
has another nice feature. Notice the rethrows
keyword in the method signature:
func sync<T>(execute work: () throws -> T) rethrows -> T
A rethrowing function indicates that it can only throw an error if one of its function parameters throws an error. This enables two things:
-
It allows the caller to pass in a throwing function (or call throwing functions in the closure expression).
-
It gives the type checker granular control over the enforcement of the
try
keyword. Callers ofDispatchQueue.sync
only have to usetry
if the passed-in function is throwing.
Replicating rethrows
for performAndWait
isn’t trivial. We’d have to catch any error inside the inner closure and pass it to the outer scope in a similar fashion as we do for the return value. If we do this though, the compiler can no longer prove the only-throws-when-function-argument-throws invariant and therefore rejects the code.
Becca Royal-Gordon just pitched to Swift Evolution to introduce a way for developers to opt out of the strict compiler checks for rethrows
, analogous to what we already have for escaping closures.
Outsmarting the type checker
In her post, Becca also mentions how the Dispatch framework solves this issue:
It is possible to work around this by exploiting certain bugs in the
rethrows
checking—the Dispatch overlay does this to add error handling toDispatchQueue.sync(execute:)
.
We can use the same trick for our problem. Check out the relevant code in the Swift repository. And here’s the verbatim copy of the code (I only changed the function names) for performAndWait
:
extension NSManagedObjectContext {
func performAndWait<T>(_ block: () throws -> T) rethrows -> T {
return try _performAndWaitHelper(
fn: performAndWait, execute: block, rescue: { throw $0 }
)
}
/// Helper function for convincing the type checker that
/// the rethrows invariant holds for performAndWait.
///
/// Source: https://github.com/apple/swift/blob/bb157a070ec6534e4b534456d208b03adc07704b/stdlib/public/SDK/Dispatch/Queue.swift#L228-L249
private func _performAndWaitHelper<T>(
fn: (() -> Void) -> Void,
execute work: () throws -> T,
rescue: ((Error) throws -> (T))) rethrows -> T
{
var result: T?
var error: Error?
withoutActuallyEscaping(work) { _work in
fn {
do {
result = try _work()
} catch let e {
error = e
}
}
}
if let e = error {
return try rescue(e)
} else {
return result!
}
}
}
performAndWait
now calls through to a private helper function that takes two throwing functions (the original block and an error handler) and this convinces the compiler that the rethrows
invariant holds.
Usage
Here’s how you’d retrieve the number of records from a fetch request:
let context: NSManagedObjectContext = ...
let fetchRequest: NSFetchRequest = ...
let count = try context.performAndWait {
return try context.count(for: fetchRequest)
}
Another nice improvement to performAndWait
would be to have it pass the managed object context to the worker block, as Michael Tsai does in his solution.
A simpler solution
Update April 22, 2019: Karim Abou Zeid suggests a simpler solution: we can provide two overloads of performAndWait
, a non-throwing one and one that takes a throwing function and rethrows any errors to its caller. The compiler is smart enough to pick the correct overload depending on whether the function the caller passes to performAndWait
can throw errors or not.
The complete code looks like this:
extension NSManagedObjectContext {
func performAndWait<T>(_ block: () throws -> T) throws -> T {
var result: Result<T, Error>?
performAndWait {
result = Result { try block() }
}
return try result!.get()
}
func performAndWait<T>(_ block: () -> T) -> T {
var result: T?
performAndWait {
result = block()
}
return result!
}
}
The small downside to this approach is that both methods will appear in code completion.