Dear Erica: Can I build a throwing operator

Another question from Ethan J. He writes, “If I wanted to implement an operator similar to ?? but one that throws when the left side is nil how would that look?” Basically he wants  the opposite of a coalescing operator that instead of offering a fallback value will raise an error when the LHS is nil.

I’m interpreting the brief here as “create a throwing version of nil coalescing specifically for use where the original code author has provided optional return values but has not updated code to work with the error handling system.”

Here’s what I came up with. !! seems a nice natural counterpart to ??.

struct Error: ErrorType { let reason: String }
infix operator !! {}

// Simple LHS optional (Thanks, Pyry Jahkola)
// Using @autoclosure is a good idea for strings with expensive interpolation (Thanks, Mike Ash)
func !!(lhs: T?, @autoclosure reason: () -> String) throws -> T {
    switch lhs {
    case .Some(let value): return value
    default: throw Error(reason: reason())
    }
}

Here are a couple of examples of successful and non-successful calls that mimic Ethan’s use case, which was to convert failable but non-throwing calls into throwing ones.

do {
    let z = try Int("x") !! "Invalid integer initializer"
    print("Value", z)
} catch { print(error) }

do {
    let z = try Int("42") !! "Invalid integer initializer"
    print("Value", z)
} catch { print(error) }

I went with string-based errors for simplicity, allowing the operator implementation to construct and throw errors using that information.

Pyry Jahkola offers an alternative approach. Instead of building a new operator, he suggests building an evaluatable RHS that could be used with nil coalescing as is:

func raise<T>(error: ErrorType) throws -> T { throw error }
func raise<T>(@autoclosure reason: () -> String) throws -> T {
    throw Error(reason: reason())
}

With this, you could use an approach similar to using && as an if-true condition:

do {
    let z = try Int("42") ?? raise("Invalid integer initializer")
    print("Value", z)
} catch { print(error) }

Using a separate function would also enable you to incorporate debug literals to capture the context of the nil result.

Notes:

  • Mike Ash mentions that he wrote an extension on Optional, something like x.orThrow(e) to do more or less the same thing.
  • Perhaps I should call !! a “nil-throwing operator”? (Thanks,  Michael M. Mayer).
  • As for the standard library, while I’d like to see a standard fallback error type, my idea of introducing a basic pre-built default error was not warmly greeted in Swift Evolution discussion threads.

5 Comments

  • What a great addition to the language. Both you and Ethan J. should be commended. I’m adding it to my snippets but I bet we see it added to 3.0. Would it be too confusing if we just called it the throw operator?

    • If you want to go that way, it would be a nil-throwing operator vs a nil-coalescing operator, no?

  • I don’t think !! is a good name for this, because ! doesn’t mean throw, it means halt execution.

  • I think you got the `@autoclosure` on the wrong side: you always want to evaluate the left-hand side. For Real Use™, I’d probably make the right-hand side simply `ErrorType` instead of `String`, as that’ll make the call site look like an error.

    But even better, why not reuse the `??` operator here and simply define a generic function that pretends to return what `??` expects:

    func raise(error: ErrorType) throws -> T { throw error() }
    
    do {
        let z = try Int("x") ?? raise(Error(reason: "Invalid integer initializer"))
        print("Value", z)
    } catch { print(error) }
    
    • I don’t like using ?? here for the same reason I don’t like using || for single condition side-effects and && as a shorthand for if-true. You raise (sorry, no pun) several good points otherwise that I will ponder.