Introducing an “Unwrap or Throw” operator

Unfortunately, I do not think this pitch is well considered. At first I thought, it may be an interesting method, spelled something like unwrapped(orThrow:)—after all, we do have unsafelyUnwrapped.

Then I realized, we'd actually want to be able to generalize this to allow a default, much like we have a dictionary subscript that allows for specifying a default value if a key isn't found. So, in like manner, we could have unwrapped(default:).

Then I realized that that'd be just ??.

The two differences between what's pitched here and ?? are: (1) It doesn't allow for the right-hand side to supply a default and in fact discards the return value. That is both inconsistent with ?? (and, for that reason, a potential footgun) and unnecessarily restrictive. (2) It uses an autoclosure, which is convenient at the point of use but is inconsistent with ??, with the tradeoff of brevity for clarity at the point of use (there is nothing about the spelling of one that signals that it differs from the other in right-hand side "autoclosure-ness").

I do not think that, even if there were not those tradeoffs and potential footguns, eliminating { ... }() at the point of use passes the bar for adding an API to the standard library, let alone a new operator. ("The bar" being those criteria laid out in the past which I will not recite again here in their entirety, but which includes asking whether the pitched API solves some performance or correctness pitfall in the status quo.) With the tradeoffs and potential footguns...

2 Likes

In evaluating the idea, it's important to separate the proposed !! operator from the semantics of the existing ?? operator as they are quite different beasts by design - even if they appear similar (If anything it's closer to the operation of ||) It's more related to and only intended to be an alternative to a force unwrap. It's better out of the stdlib as using it as a Package has the "failing fast during development" advantages I mentioned above. There is a longhand unwrap(orThrow:) available in the code above but it is not at all pretty to use.

I don't understand. Both ?? and this !! use @autoclosure for their right-hand side.

1 Like

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.