How to write a function that returns the same type as one of its arguments?

Hi All.

It's hard to explain in words, so I'll just present the example. I'm trying to write a function like so: (an extension on NSManagedObjectContext)

@inlinable public func hxPerformAndWait<T>(_ block:(NSManagedObjectContext)throws->T) throws -> T {
    var blockError:Error? = nil
    var retVal:T? = nil
    
    self.performAndWait {
        do {
            retVal = try block(self)
        } catch {
            blockError = error
        }
    }
    
    if let someError = blockError {
        throw someError
    }
    
    if let val = retVal {
        return val
    }
}

I'm trying to take a function which executes a non-returning, non-throwing block and wrap it so that you can send it an (optionally) returning and throwing function. The problem here is with retVal. If I don't declare it optional, I can't initialize it to make it captured when block gets executed. If I do declare it optional, then I have to unwrap it before returning it. But then I can't return it when there's nothing to unwrap because I have to return "T", and not nil. So is this function just impossible to write?

Thanks!

This is a case where you could write return retVal! instead of conditionally unwrapping.

2 Likes

Ah, Thanks! One shortcut begets another. Who knew condensing down to one line could save you some trouble. I'm fundamentally opposed to using "!", so with all of my little shortcuts in place, it now looks like this:

@inlinable public func hxPerformAndWait<T>(_ block:(NSManagedObjectContext)throws->T) throws -> T {
    var blockError:Error? = nil
    var retVal:T? = nil
    
    self.performAndWait {
        do {
            retVal = try block(self)
        } catch {
            blockError = error
        }
    }
    try rethrow(blockError)
    return try retVal ?? {throw HXErrors.internalInconsistency("executed block returns nil. This should never happen")}
}

Also I'm trusting that this will all go right if T happens to be Optional<X>.

It's not correct to throw if retVal is nil, because it should never happen and would represent an unrecoverable logic error. Throwing in Swift is for those errors for which recovery is possible, since it will need to be caught.

You could use fatalError(), but the idiomatic way of spelling that assertion is !, either where you return or initially when you define the variable (i.e., as an implicitly wrapped optional). There is a reason that this operator exists in the language.

7 Likes

Exactly. You shouldn't be "fundamentally opposed to using !" to the point of obviously making your code worse. The reasonable thing to do is to see ! as a feature that should be used rarely and only with good reason. But it is inevitable that sometimes you will know things that the compiler does not, and in those cases it's okay to use asserting features like ! or as!.

7 Likes

There is no guarantee (at the language level) that performAndWait will always execute the closure (and that it won't return before the closure completes). If for some reason that doesn't happen, you would end up returning an uninitialised variable, which is not legal in Swift.

Right. And on a language level, it could also be run multiple times, which would cause related problems. But since you do have that guarantee that the initialization will happen, an assertion is a reasonable way to handle it.

1 Like

Point taken. Thanks!

Just a couple more things on using the exclamation mark:

  • I never use the exclamation mark to unwrap in my code for two reasons:
    • if you use fatalError, you can put in a message
    • in the future, it will be much easier to search for the word fatalError than an exclamation mark
  • It's often a struggle for me to decide whether a particular error is fatal or not. When you're writing an app, you don't want it to just go away on the user, so I tend to lean towards throwing an error. That being said, it's probably better to take some time out to consider what's recoverable and what's not. I think I'm finally getting to a point with Swift that I never got to with Objective-C: how to write code that is readable, handles recoverable errors properly, and does not hide those errors from the user.
2 Likes

@hexdreamer If you excuse the shameless self-plug, I wrote a blog post a while ago that includes a workaround to make this performAndWait variant work with rethrows, thereby not forcing the caller to use try unless they pass in a throwing function: A better NSManagedObjectContext​.performAndWait.

I learned this workaround from @beccadax.

Thanks! Always ready to make it better. I've already managed to turn some almost unreadable code into something more readable.

If you insist on avoiding ! – which I don’t think is a bad idea – you’ll need a guard statement before the return:

guard let retVal= retVal else { fatalError(“This should never happen”) }
return retVal