[Pitch] Optional.orThrow

In every Swift project, I find myself needing this function:

extension Optional {
    func orThrow<E: Error>(
        _ error: @autoclosure () -> E
    ) throws(E) -> Wrapped {
        guard let wrapped = self else {
            throw error()
        }
        return wrapped
    }
}

I'd like to propose adding this API to the stdlib, so you can write

let thing = optionalThing.orThrow(MyError.missingThing)

Which is often more useful than the more verbose,

guard let thing = optionalThing else {
    throw MyError.missingThing
}

For example, where several things are optional:

try myFunction(
    a: optionalA.orThrow(MyError.missingA),
    b: optionalB.orThrow(MyError.missingB),
    c: optionalC.orThrow(MyError.missingC)
)

it's a lot more succinct and easier to follow than the solution with guard:

guard let a = optionalA else {
    throw MyError.missingA
}
guard let b = optionalB else {
    throw MyError.missingB
}
guard let c = optionalC else {
    throw MyError.missingC
}
myFunction(a: a, b: b, c: c)

Random thoughts:

The name, obviously, is not the only possibility. That's the name I choose, and the name I found already present when I joined my current job, but eg. unwrap(orThrow:) might be another good option.

Pointfreeco use a slightly different formulation, that I think is less flexible, and wouldn't cover some of the cases I use it for:

extension Optional {
  struct Nil: Error {}

  public func unwrap() throws -> Wrapped {
    guard let unwrapped = self else { throw Nil() }
    return unwrapped
  }
}

Proposing the addition of this function raises the question of whether we should also add the missing Optional to Result conversion for completeness:

From :arrow_down: To :arrow_right: Optional<T> Result<T, E> throws(E) -> T
Optional<T> n/a (opt.orThrow(e) proposed above)
Result<T, E> try? result.get() n/a result.get()
throws(E) -> T try? f() Result { try f() } n/a

(you could also reasonably add Bool to this table, in which case there's a few more missing conversions)

4 Likes

This wouldn't be necessary if throw were an expression of arbitrary type:

optionalThing ?? throw MyError.missingThing
8 Likes

You can currently write:

optionalThing ?? { throw MyError.missingThing }()
5 Likes

I had considered that you could add an overload of ??, like

func ?? <T, E: Error>(
    lhs: Optional<T>,
    rhs: @autoclosure () -> E
) throws(E) -> T {
    guard let lhs else { throw rhs() }
    return lhs
}

allowing

let thing = try optionalThing ?? MyError.missingThing

But is that a more natural way of expressing this? I wasn't convinced. It also proliferates ad-hoc operator overloads, which is pretty bad for compile times in Swift.

See also previous threads on this topic:

In particular, re @ellie20's comment, see:

7 Likes

Seems like, if one were to summarize those threads, a reasonable conclusion would be that "treat throw as an expression with an arbitrary type" is the preferred option. Allowing, as Ellie said,

let thing = optionalThing ?? throw MyError.missingThing

(Of course, it's a much harder proposal & implementation than what I originally suggested, which may be why it hasn't happened yet despite the interest :sweat_smile:)

4 Likes

I guess "throw is an expression" still has two possible interpretations;

  1. The type of a throw expression is always Never
    • This implies an additional overload of ?? which takes @autoclosure () throws(E) -> Never on the right-hand-side, I think
    • Which isn't much better than one of the options in the thread above
    • And allows shenanigans like optionalThing ?? fatalError(), which is maybe actually quite nice?
  2. The type of a throw expression is something else (people in the other threads are calling this option a "true bottom" type; a type that is a subtype of all types.
    • Swift doesn't have this concept
    • I guess there might be other options, such as forcing it to Never when it appears in a statement, but allowing it to live unconstrained and take on any type in non-statement contexts?

(and you can of course combine the two by taking the tack of "make Never be a true bottom type", which I think has also been discussed elsewhere...)

3 Likes

This currently compiles:

func f<T, E: Error>(closure: () throws(E) -> T) throws(E) -> T {
    try closure()
}

enum MyError: Error { case missingThing }

// closure is inferred as
// () throws(MyError) -> Int
let x: Int = try! f { throw MyError.missingThing } 

// same closure literal is *currently* inferred as
// () throws -> (),
// which is interesting, I guess it should eventually be
// () throws(MyError) -> () but why not
// () throws(MyError) -> Never?
let c = { throw MyError.missingThing } 

But I guess that means it's fine for hypothetical throw expressions to behave the same, no need either for a bottom type or to say they always have type Never?

throw in Swift is (currently) a statement, not an expression (as discussed), and closure type-checking happens before reachable code analysis. So there's nothing to prefer Never over a closure's default Void, though that's a rule that could potentially be changed (after investigating to see how source-breaking it is).

2 Likes