[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
10 Likes

You can currently write:

optionalThing ?? { throw MyError.missingThing }()
6 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:

9 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:)

5 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).

3 Likes

I see what you’re getting at here and have done similar in Kotlin before now.

But, seeing the guard examples you added right next to the proposed idea just make me reassured the guard is doing the job it was designed to do and is still the best solution for this… guard else throw

I think there’s an equal reasoning for doing a .orFatalError() here which is ultimately doing the same thing and might also be a good example as to why the suggestion you gave doesn’t quite make sense to me

I didn't realize this was possible (and I haven't taken the time to think through why it works), but that's pretty cool.

In use it would look a little strange because it's unfamiliar:

try myFunction(
    a: optionalA ?? { throw MyError.missingA }(),
    b: optionalB ?? { throw MyError.missingB }(),
    c: optionalC ?? { throw MyError.missingC }()
)

but it ends up very close to the original example:

This seems to retread the same ground as "Unwrap or die" pitch and rejected proposal SE-0217 circa 2018, and spin-off discussions that followed about ?? and Never expressions like throws and fatalError on the r.h.s.

Digging up my relevant 2¢ on this from one such spin-off thread: Make Never the bottom type - #34 by jpmhouston" (with new clarification/relevance in square brackets):

It makes sense to me that this operator is always [optional-] value ?? value. By permitting [throws on the r.h.s.] – statements really – with semantics that more aligns with a force unwrap, I think it would chip away at my mental models.

[If ?? throw becomes legal Swift] then I for one will be instead defining my own !! operator as in SE-0217 so that I can use it in place of ?? in those instances:

[rewriting the example to fit in the topic of this thread]

let x = y !! throw(MyError.missingThing)

Or when I'm feeling cheeky:

let x = y !! MyError.missingThing

(edit: and imagine "try" inserted somewhere appropriately above, thanks @christopherweems. I always forget "try" when there's no compiler to remind me)

1 Like

@jpmhouston I like this a lot and agree per your rationale that overloading ?? is messy conceptually. My only suggestion for a new operator like !! is that try should be explicitly spelled out:

let x = try y !! MyError.missingThing
1 Like

+1 for these improvements

In my project I use the following:

extension Optional {
  public func unwrapOrThrow(_ errorExpression: @autoclosure () -> some Error) throws -> Wrapped { ... }
  
  public func unwrapOrThrow(_ errorExpression: () -> some Error) throws -> Wrapped { ... }

  public func asResult<E: Error>(replacingNilWith error: @autoclosure () -> E) -> Result<Wrapped, E> { ... }
}

It seems that we independently came to almost identical solutions.
Dealing this way with lots of optional values in throwable context make code significantly cleaner in my opinion.

I also understand it can be my personal preference.

// Sorry for a verbose example, I want to show how it can look in a real code
init(urlString: String) throws {
  let url = try URL(string: urlString).unwrapOrThrow(AppLinkError(code: .invalidURLString, info: ["urlString": urlString]))
  let scheme = try url.scheme.unwrapOrThrow(AppLinkError(code: .missingScheme, url: url))
  let host = try url.host.unwrapOrThrow(AppLinkError(code: .missingHost, url: url))
  let urlComponents = try URLComponents(url: url, resolvingAgainstBaseURL: true)
    .unwrapOrThrow(AppLinkError(errorCode: .cantCreateURLComponents, url: url))

  let (discriminant, pathComponents) = try Discriminant.make(urlPath: url.path)
    .unwrapOrThrow(AppLinkError(code: .noMatchingDiscriminant, url: url))

  let searchText = try urlComponents.valueOfQueryItem(withName: "text")
    .unwrapOrThrow(AppLinkError(errorCode: .missingQueryItem, url: url))
  let promoId = try pathComponents.first.flatMap { UInt64($0) }.unwrapOrThrow(pathComponentError)
  ...
}

Can we treat such code idiomatic? unwrapOrThrow is used here for early exit, but we already have guard-else statement for this purpose.
To add such functions into standard library we need to research for downsides of such approach and be sure there is rationale of using it as common solution.

It is interesting to hear an opinion of language steering group about it.

2 Likes

Is this good enough?

try x ?? BIKE_SHED_NAME_HERE(SomeError.error)

Don't know how to name it. This is possible today:

func BIKE_SHED_NAME_HERE<T>(_ error: Error) throws -> T {
    throw error
}

e.g.:

try x ?? throwError(FileManagerError.fileNotFound)

Or this:

extension Error {
    func `throw`<T>() throws -> T { throw self }
}

to then write:

try x ?? FileManagerError.fileNotFound.throw()

Interestingly in this case I didn't need to backtick my "throw" function at the use site.


Or just this?

func ?? <T> (lhs: T?, rhs: Error) throws -> T {
    if let lhs { return lhs }
    throw rhs
}

to use it this way:

try x ?? FileManagerError.fileNotFound

(the last one was suggested above by @KeithBauerANZ)

I assume overload of nil coalescing operator ?? will be rejected because of type checker complexity involved.
Currently there are several rethrowable overloads in standard library:

func ?? <T>(T?, () throws -> T) rethrows -> T
func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?

func valueOrThrow() throws -> Int { 0 }

let value = try optionalVal ?? valueOrThrow()

My Swift feeling is that these overloads are for edges cases.
Nil coalescing operator:

  • normally returns a value
  • both left and right operands are:
    • a value or an expression returning a value. Even though there are overloads for throwing expressions, those expression still have a return a value.
    • both arguments are of the same type T

func ?? <T> (lhs: T?, rhs: Error) throws -> T in contrast

  • have right operand of another Type
  • there is no value for returning from right operand

During a day to day coding most developers expect ?? to return a value.

The term nil coalescing operator tells much about its purpose.
It is not about early exit, which seems to be the root reason of introducing new convenient syntax discussed here.

I would better suggest something like ?! operator if we have a consensus that unwrap(orThrow:) and similar variants are too verbose:

init(urlString: String) throws {
  let url = try URL(string: urlString) ?! AppLinkError(code: .invalidURLString, info: ["urlString": urlString])
  let scheme = try url.scheme ?! AppLinkError(code: .missingScheme, url: url)
  let host = try url.host ?! AppLinkError(code: .missingHost, url: url)
  let urlComponents = try URLComponents(url: url, resolvingAgainstBaseURL: true) 
    ?! AppLinkError(errorCode: .cantCreateURLComponents, url: url)

  ...
}

with some more conveniency for typed throws

init(urlString: String) throws(AppLinkError) {
  let url = try URL(string: urlString) ?! .invalidURLString
  let scheme = try url.scheme ?! .missingScheme
  ...
}

In this case:

  • we have another operator for another purpose
  • overload resolution complexity of ?? is eliminated
  • ? symbol in ?! make it visualy related to nil coalescing but syntactically different.
  • ! symbol in ?! explicitly says that operation is throwable
  • ? and ! symbols in ?! tell that left and right operands are completely different

The main point I want to convey is that nil coalescing operator seems to be not suitable and we should explore another variants.

1 Like

Whilst I agree that ?? doesn't make the best sense (and it's definitely making me reconsider that maybe the original post has the right idea, rather than pursuing allowing throw expressions to take on any type), I don't think anything with ! is correct for Swift either — ! appended to something means "or crash" in Swift, which this operation does not do.

1 Like

! is not always about crash.
Of course try! and as! can cause a crash but not necessarily.
var value: Int! declaration itself is absolutely safe. A crash can possibly appear on access to the variable if it is nil
With boolean values ! has a completely different meaning – logical not (nothing about crashes)

In all of the cases above ? and ! are not used together. Their separate usage has own meanings.
Single ? symbol have completely different meanings (e.g. in as? try?) than double ??
The ?! operator use these symbols together so the meaning is also different. There is no new rules introduced.

I'm also not totally happy with ! appended. May be you have some alternative suggestions

Worth to note: there's a critical difference between x! and anything we've brought up so far: x! could be used as an L-value:

x! = value
x!.mutatingFunc()
x![1] = value
x!.field = value

whereas

try x ?? throw error = value

or a similar syntax would result in:

// :stop_sign: Cannot assign to value: binary operator returns immutable value

Would be cool if the "throwing optional unwrap" equivalent supported L-value usage scenario.


In light of the L-value issue, here's another suggestion: introduce a special built-in postfix ^ "operator", that for all intents and purposes works like ! except it throws on nil instead of terminating the app:

try x^.mutatingFunction()
try x^ = value
try x^[1] = value
try x^.field = value
value = try x^

When the value is nil it'd throw some fixed non customisable "Nil" error.

That'd require a compiler change as it can't be done by current language means.

Similarly as^ could be a throwing version of as!