Introducing an “Unwrap or Throw” operator

Is there a technical reason for why the statement cannot be captured by @autoclosure?

In fact I think that in theory the following signature rhs: @autoclosure () throws -> Never should be sufficient to solve the problem. The compiler only needs to learn how to wrap throw error.

I added Never because we want to signal that the captured closure always throws but never returns.

Then this should do the whole trick:

func ?? <T>(lhs: T?, rhs: @autoclosure () throws -> Never) rethrows -> T {
  guard let unwrapped = lhs else {
    try rhs()
  }
  return unwrapped
}

This is a distinction the compiler makes internally at a very low level: declarations, statements (flow of control, throw return etc.) and expressions (function calls which include assignments as they are all just overloaded operators). Operators can currently only operate on expressions.

I might misunderstand here something. I still don't see why @autoclosure should not be able to capture whole statements.

// everyone knows that @autoclosure will wrap everything into a block / closure
throw myError ---> { throw myError } 

We already can use the closure syntax without @autoclosure and get the expected result. All we want to achieve here is to make it a bit more convenient and implicit.

And if we're at it, I think it might be a great idea to introduce a tiny breaking change to the language for Swift 6:

let foo = {
  throw MyError.bar
}

// currently: `foo: () throws -> Void`
// after: `foo: () throws -> Never` 
// If the compiler knows that every exit path of a function throws
// the function itself should never return, hence `Never`

let bar: () throws -> Void = {
  if someCondition {
    print("swift")
  } else {
    throw MyError.bar
  }
}

I move the last idea into its own pitch. ;)

1 Like

Good point: a think-o on my part. That one autoclosure works with throws and the other doesn’t is the inconsistency then—and I see folks are exploring why that is.

I want part of this functionality, but...

  • It shouldn't be an operator.
  • It doesn't need to handle every case proposed, given how rare many of them are. In particular, I don't think it needs to handle fatal errors or custom messages by default.

So, as @xwu mentioned, I think this functionality should simply be unwrapped(orThrow:) that produces a new UnwrapError or a provided Error instance. I add this to all of my projects anyway, as it's a good way to bridge optional and throwing code without having to manually unwrap everything.

1 Like

I have been using this solution for ages and haven't felt a need for a custom operator or a method on Optional:

A standard UnexpectedNil error might be nice though.

7 Likes

Thanks @mayoff! That is a very, very useful link. It's all there from 2018 including discussions about throw as an expression etc. I've pushed a new version of the Unwrap Swift Package with my take on @Joe_Groff's suggestion (with all the elegance and simplicity taken out:)...

/// Function that allows you to throw but in an expression.
/// Intended for use after nil coalescing operator in unwrap.
/// Gives fatalError in Debug builds to aid debugging and
/// throws in a Release build for app to be able to recover.
/// - Parameters:
///   - toThrow: A String, Error or closure that throws
/// - Throws: Error passed in or UnwrapError
/// - Returns: Never
public func unwrapFailure<E,T>(throw toThrow: E,
    file: StaticString = #file, line: UInt = #line) throws -> T {
    if let throwingClosure = toThrow as? () throws -> Void {
        try throwingClosure()
    }
    let toThrow = toThrow as? Error ?? Optional<T>.UnwrapError
        .forceUnwrapFailed(text: "\(toThrow)", type: Optional<T>.self,
            file: file, line: line)
    #if DEBUG
    // Fail quickly during development.
    fatalError("\(toThrow)", file: file, line: line)
    #else
    // Log and throw for a release build.
    // Gives the app a chance to recover.
    NSLog("\(toThrow)")
    throw toThrow
    #endif
}

Something like this probably should be in the standard library and more widely known even if it is a global function. You can indeed now just type the following which seems a pretty good solution:

try someOptional ?? unwrapFailure(throw: myError)
1 Like

I sometimes use this in my code bases:

@_disfavoredOverload
public func ??<Wrapped>(optional: Wrapped?, error: @autoclosure () -> Error) throws -> Wrapped {
    guard let wrapped = optional else { throw error() }
    return wrapped
}
3 Likes

I agree that the unwrap failure is ergonomic. There should be better debugging tooling for nil unwrapping. I once had a crash in an App Store submission for AR MultiPendulum, and I had to go through a lengthy crash log without any description of the error. I think it was from a failed nil unwrapping.

In my code, I tend to use something like this, to both give better crash logs, as well as self-document at call site:

extension Optional {
  func safelyUnwrapped(because reason: String) -> Wrapped {
    guard let unwrapped = self else {
      fatalError("Failed non-nil assertion: \(reason)")
    }
    return unwrapped
  }
}

Then at call site, e.g:

let url = URL("http://swift.org").safelyUnwrapped(because: "url is known at compile-time to be valid")
1 Like

Am I correct in thinking that there are multiple different desires in play?

  1. "unwrap or throw" as just sugar to transform an optional unwrap to a throwing error.
  2. "unwrap or die" alternatives to force unwrapping (and crashing if nil) because we want to be explicit about the reasons.

I see these things as separate goals. The linked package seems to focus on number 2 because it fatalError on DEBUG.

I personally always want 1, never 2. I want sugar to throw an error if nil, but that's something I expect to happen, is not an edge case that needs to fatal error.

IMO ideally we should have both behaviours using the same syntax and being explicit.

try optional ?? throw(Error())
optional ?? fatalError("fancy message")
1 Like

Ideally, Never would be a proper bottom type and the second syntax would just work out of the box. As for the first syntax, there's nothing wrong with it, but I'm not entirely sure if it's a common enough need to be in the standard library.

While I generally dislike NPM style micro-libraries, it might be best to put both versions of ?? into a library and see how often people use it. The implementation is actually quite simple:

@inlinable
public func ?? <T>(value: T?, failure: @autoclosure () -> Never) -> T {
    guard let value = value else { failure() }
    return value
}

@inlinable
public func ?? <T, E>(value: T?, error: @autoclosure () -> E) throws -> T where E: Error {
    guard let value = value else { throw error() }
    return value
}

EDIT: Actually, it seems that the Never version does work out of the box now, but you still need to include the definition for it if you want to use both that and the Error version, because otherwise it'll try and call the throwing version when you put fatalError on the RHS.

1 Like

Without commenting on the larger proposal, I’ll note that while throw is a statement today, there’s nothing that requires it to be one. Since you can already put try immediatelyThrow(error) into expression position I don’t think there’s any ideological reason to keep throw error from being a valid expression.

7 Likes

+1 for this, I'm using it for several years. Here is the sample:

extension Optional {
  public func unwrapOrThrow(_ errorExpression: @autoclosure () -> Error) throws -> Wrapped {
    guard let value = self else { throw errorExpression() }
    return value
  }
  
  public func unwrapOrThrow(_ errorExpression: () -> Error) throws -> Wrapped {
    guard let value = self else { throw errorExpression() }
    return value
  }
}

// Usage:
let url = try urlComponents.url.unwrapOrThrow(AppLinkError(code: .unexpectedNilURL))
let deepLinkString = try (deepLinkURLValue as? String).unwrapOrThrow(AppLinkError(code: .typeCastFailed))

Using #file and #line is not good default option, because it increases binary size. As a tradeoff optional values win nil default value can be used.

public func unwrapOrThrow(_ errorExpression: @autoclosure () -> Error, file: StaticString? = nil, line: UInt? = nil) throws -> Wrapped

let value = try optional.unwrapOrThrow(MyError())
let value = try optional.unwrapOrThrow(MyError(), file: #fileID, line: #line)
1 Like

-1 here. I'm not seeing the need for a new operator. I'm doing this for years and it works/chains great:
try URL(string: "https://google.com").unwrap(). You could overload .unwrap() to satisfy all scenarios above, and in a clearer way (i.e. using named params).
Personally, I would be in favour of adding an .unwrap() method (or similar) on Optional, since this is something I use in almost all my projects. Not sure how it would fit others tho.

Same here. I don't like adding a new operator for this. Seems like most of us already use this "unwrap or throw" method on Optional and are happy with it. And yes, I would like to have the standard library featuring this method.

An unwrapOrThrow method certainly plays nicer with the pre-existing ability to put a Never returning function on the left hand side of ??. It's also slightly more explicit about the fact that the supplied error will be thrown.

Recently, someone pointed out to me that, if you squint a lot, Optional is just Result where Failure is nil kind of thing. Given this, the "correct" function to use would be try get().

Using the following generic conversion functions foo! is equivalent to try! foo.get() with try? foo.get() being a full round trip.

public extension Optional {
    enum UnwrappingError : Error {
        case foundNilWhileUnwrappingAnOptionalValue
    }
    
    func get(orThrow error: @autoclosure () -> Error = UnwrappingError.foundNilWhileUnwrappingAnOptionalValue) throws -> Wrapped {
        if let value = self {
            return value
        } else {
            throw error()
        }
    }
}

public extension Result {
    init(_ optional: Success?, failure: @autoclosure () -> Failure) {
        if let success = optional {
            self = .success(success)
        } else {
            self = .failure(failure())
        }
    }
}

public extension Result where Failure == Error {
    init(_ optional: Success?) {
        self = Result {
            try optional.get()
        }
    }
}

It is indeed. If Result was there first we would probably not invent Optional.

Why would I want to use a longer try! foo.get() than a shorter foo! ?

Very much agree, I was just pointing out that they are equivalent rather than suggesting one use the longer form.

Another example is if let foo = foo being equivalent to if case let .some(foo) = foo, although again that shows the similarity's between Optional and Result.