Unwrap or Throw - Make the Safe Choice Easier

The !! unwrap or die discussion, pointed out there is another way to handle optionals. Right now we have no language support to throw if the item is nil. this would be preferable than only having !! for the following reasons (although would not reduce the usefulness of that when you don't want to make your method throwable):

  • A single line solution, rather than guard let or if let or if value != nil
  • Makes it easier to do the right thing, that is, write safe code, by handling the nil rather than force unwrapping or ignoring it.
  • Completes the matrix of your choices when dealing with an optional.
  • Users can consolidate common error scenarios by implementing Error No need for repeating the same string in multiple places. They would use a shared enum thus making for cleaner code
  • In terms of code size, the happy path would not be dwarfed, by the error path.
  • It utilizes existing try catch throw mechanisms.

It is possible to do without an operator by creating a Thrower struct, you can see in the first example. This is a generic class which just throws the error, admittedly not convenient and wordy. So it seems that a language change is necessary (unless others have better approaches). The examples after that show alternatives for a possible syntax to support such a feature.

enum Errors: Error {
    case falseResult
}

func throwIfNil(value: Int?) throws {
    try value ?? Thrower().throw(value, Errors.falseResult)  // works in Swift today
    try value ?? throw Errors.falseResult                    // proposed
}

struct Thrower<T> {
    // need this to return same type as value, but not used
    func `throw`(_ item: T, _ error: Error) throws -> T  {
        throw error
   }
}

edit - removed other syntaxes that I no longer think are relevant.

I don't think we should use ?! or similar because ! should always mean unsafe (unless it is a not operator). This will be a safe operation.

The main idea is to eliminate most uses of force unwrap, and make it almost as easy to write safe code where you have nil coalescing,guard let and if let as your complete suite of choices.

Would love to get some feedback or discussion of whether this is.a interesting idea.

2 Likes

I use this in basically all projects:

struct NilError: Error {}

extension Optional {
    func unwrap(or error: @autoclosure () -> Error = NilError()) throws -> Wrapped {
        switch self {
        case .some(let w): return w
        case .none: throw error()
        }
    }
}

I've been quite happy with this. In some places a little sugar might be nice, in others the chainability of try lol.unwrap(or: SendHelp()).something() would still win.

edit: Looking at this piece of code again, I might actually change NilError like this, to give pretty great diagnosability, even when no custom error is provided:

first revision, see below
struct NilError: Error {
    let file: String
    let line: Int
    let column: Int
    let function: String
    
    init(file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) {
        self.file = file
        self.line = line
        self.column = column
        self.function = function
    }
}

edit 2: scratch that, #file, #line, ... don't currently seem to propagate through multiple levels of default arguments, so this is what I use now:

struct NilError: Error {
    let file: String
    let line: Int
    let column: Int
    let function: String
}

extension Optional {
    func unwrap(file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) throws -> Wrapped {
        return try unwrap(or: NilError(file: file, line: line, column: column, function: function))
    }

    func unwrap(or error: @autoclosure () -> Error) throws -> Wrapped {
        switch self {
        case .some(let w): return w
        case .none: throw error()
        }
    }
}
2 Likes

If we go the route of Never-as-universal-subtype, then it'd be reasonable to also make throw an expression of type Never, which would let you write things like x ?? throw error, as in C++, Kotlin, and other languages where throw is an expression.

27 Likes

Interesting, this is similar to the Unwrap or Die discussion, Chris and others suggested the unwrap extension. as an alternative to !! except you are throwing an exception instead of failing. This will only throw, the one exception, but maybe that is sufficient with the line and file name. I agree sugar would be nice here.

Yes, thank you for expressing it. Throw needs to be an expression and the Never support would be perfect for that. Sounding like this idea might not be very controversial. :-)

It looks like Never is moving forward so it looks like this should be very easy to implement with that support. I suggest we that we make throw an expression and it implements Never as Joe mentions above. Does anyone have any suggestions or improvements which would enable the following? And then making a sample implementation if it looks like a good direction?

enum Errors: Error {
    case falseResult
}

func throwIfNil(value: Int?) throws {
      try value ?? throw Errors.falseResult     
 }

You could play with the idiom by putting a simple wrapper function around throw today:

// could be -> Never when Never-as-bottom is implemented
func raise<T>(_ error: Error) throws -> T {
  throw error
}

try value ?? raise(Errors.falseResult)

We originally decided against making throw an expression because it would be inconsistent with other control flow statements like break, continue, and return. With Never in the type system, and rethrows autoclosures, we could conceivably make it so that break, continue, and return were also expressions, and plumb them through autoclosures as if they were thrown errors so that they can branch out to the right places in the outer function. This would allow ?? to be used as shorthand for a lot of trivial one-line guard lets that unwrap an optional or immediately exit scope on nil.

11 Likes

+1

-Chris

4 Likes

That was part of it, but we also didn't make throw an expression because we didn't have Never, and it wasn't a bottom type, so doing this wouldn't have had any value.

It would be very interesting to do this, I'd (tentatively) love to see it. I don't see any reason to limit it to autoclosures though, normal closures could participate as well. We'd just have to have some sort of attribute or other indicator on the closure function type.

-Chris

3 Likes

Sure, we could definitely support break/continuing out of general closures. return is a little more interesting because there's ambiguity as to which context you'd want to return out of. Autoclosures make the design space a bit simpler because it's (IMO) always obvious which function you'd want to return from, since there's no reason to explicitly return out of an autoclosure, and also because autoclosures can only propagate errors via rethrows, and rethrows functions can currently only propagate errors, not handle or originate errors on their own, which avoids (I was mistaken about this). There's the question of whether the break/continue pseudo-exception ought to be interceptable by intermediate frames.

1 Like

I can't say I'm a fan of adding anything more to ??, as it would become more than a nil-coalescing operator at that point.

However, I am a huge fan of optional throwing so we can use try optional.unwrap(). @ahti's solution is pretty great, and I usually implement a version that doesn't take a custom error.

2 Likes

This wouldn't be adding anything to ?? specifically, to be clear. You'd be able to use Never expressions anywhere as a placeholder where a value isn't available and exiting scope is the only choice.

3 Likes

This is not exactly the case:

enum CatError: Error { case hairball }
enum DogError: Error { case chasedSkunk }

func foo(_ f: () throws -> Void) rethrows {
    do { try f() }
    catch { throw CatError.hairball }
}

do {
    try foo{ throw DogError.chasedSkunk }
} catch {
    print(error) // hairball
}

The above rethrows function does not propagate the thrown error, instead creating a new error.

Moreover, a rethrows function is also currently allowed to throw if a local function inside it throwsβ€”although this will crash the app if a non-throwing closure was passed to the outer function and the local function does throw.

And also, DispatchQueue.sync (which rethrows) is implemented as simply calling through to a private helper method _syncHelper which is also rethrows, by passing in an unconditionally-throwing closure. The way _syncHelper is written it does in fact only throw errors that originated from the closure passed to sync but that does not have to be the case.

3 Likes

Ah, I didn't realize we had implemented that.

1 Like

This sounds amazing to be honest. Having a simply one liner for guard let x = .. else { return } would be so useful.

2 Likes

I think break, continue, and return as well as throw as expressions would be great. I suspect some means of distinguishing return from inner closure/function and return from an outer closure/function will be needed. Two possibilities come to mind:

  1. Introduce continue (with optional value) to mean exit from innermost closure/function and re-define return to mean exit from outermost closure/function only. This would be a breaking change (closures currently use return) but would match what other languages do, though often called yield, and importantly also allow if, for, while, do, try, and switch to become expressions. The continue could also be labelled, see next suggestion.

  2. Introduce labelled returns for when an exit is not from the innermost closure/function, i.e. functionName.return means that the return is from functionName and not just the inner closure/function. Similarly labels on for etc. and for closures that are declared via a let statement can be used for the labelled return. An unlabelled return would behave as it does at present.

I like the power of option 1 and would therefore go with that (a first stage without labelling might be a practical starting point).

I thought I would try to compile in a playground to see what it might look like. I created some names which are expressions, with an _ after. Not working code, just trying to see what it looks like. I really think this is nice.

// could be -> Never when Never-as-bottom is implemented
func throw_<T>(_ error: Error) throws -> T {
    throw error
}

enum Choices {
    case one
    case two
    case three
}

var continue_ = 0
var return_ = 0
var break_ = 0

 for g in 0..<10 {
    let h: Int? = g
    let p = h ?? continue_
    let q = h ?? break_
    let r = h ?? throw_(Errors.falseResult)
    let s = h ?? return_
}

let c = Choices.one

func call() throws {
     let val: Int? = nil
     let a = val ?? return_
     let b = val ?? throw_(Errors.falseResult)

     switch c {
     case .one:
         let c = val ?? break_
     case .two:
        let d = val  ?? return_
      case .three where val ?? break_: // as expected, will not compile
         print()
     }
 }

// also, think this might work.
func call2() -> Int  {
    let val: Int? = nil
    let a = val ?? return_(10)
}

I agree making other control flow statements into expressions would also be nice.

1 Like

Control flow as expressions would be great for unit testing. Test code usually just cares about the one, small thing it is testing, and anything unexpected should fail the entire test. At the moment, functions like XCTAssertNoThrow have no way to bail out of the enclosing test function if they fail.

For example:

fun returnsOptionalVal() -> MyVal? { ... }

// Currently:
guard let myVal = returnsOptionalVal() else {
  XCTFail("unexpected nil")
  return
}

// Could be:
let myVal = XCTAssertNotNil(returnsOptionalVal()) // else: fail, return Void.

It can make writing tests a pain, because although specialised functions like XCTAssertNoThrow provide MUCH better diagnostics, they just totally impractical to use without the ability to return from the function. For example, the only way I found to use XCTAssertNoThrow with a throwing initialiser is via a temporary optional:

struct MyVal { init() throws { ... } }

var _myVal: MyVal?
XCTAssertNoThrow(_myVal = try MyVal())
let myVal = _myVal!

// I want to write:
let myVal = XCTAssertNoThrow(try MyVal()) // else: fail, return Void.
2 Likes

@Karl, you can add the throws keyword to your test method, and it will fail if an error is caught by the XCTest framework (at least on macOS).

class MyValTests: XCTestCase {
  func testMyVal() throws {
    let myVal = try MyVal()
    /* ... */
  }
}
3 Likes
Terms of Service

Privacy Policy

Cookie Policy