Throw on nil

Oops; sorry for missing that!

Some really cool ideas. This is a lot of fun.

As @Jon_Shier points out, there is not really a precedent for or* operations so it is probably not a very good name! Sorry! On the other hand, a throwing unwrapped() on Optional seems very natural to me, and if you said its been there since Swift 2, I would have believed you. :slight_smile:

Because unwrapped() feels like such an obvious and natural abstraction to me now, I think it can (and should?) be considered independently of making throws a bottom type that can be used with ??. If I understand the issues correctly, I don't think that having this unwrapped function walls off this improvement to the type checker as a future feature, and it addresses an existential, day-to-day problem.

I like the nested name Missing error the best. Great observation from Rob @mayoff that it's going to be generic and harder to catch. I kind of feel like in this case where you want to be real specific, though, the caller should be handling the optional differently, either with a tried and true guard let or throwing a custom, remapped error that is more "catchable." Fortunately, though, you can still catch it if you have to.

The overload solution from @noremac is pretty great because it makes errors immediately useful to those just starting out. It encourages playgrounders to embrace error handling instead of try! or compact mapping away errors the name of, "hey, this is just test playground code" only to have it copy and pasted into production code and then shipped! :sweat_smile:

I do not understand what the term “bottom type” means, but if it turns throws into being used at the end of ?? for nil coalescing, I support this, though maybe only if it’s inside an appropriate context, like only inside a do block.

The bottom type concept, where Never is an example, is how we can use preconditionFailure or fatalError to end a value-returning function without screwing up the type checker.

1 Like

I think all the argument-specific versions of Optional.MissingValueError should directly refine Error, but of a general Optional error.

protocol MissingOptionalValueError: Error {}

extension Optional {
    struct MissingValueError: MissingOptionalValueError, CustomStringConvertible { /*...*/ }
    //...
}

This way people who want per-Wrapped and Wrapped-agnostic handling of Optional screw-ups can be happy.

Can throw (or a new function that throws) be turned into a pseudo bottom type like this undefined<A> generic function is in this StackOverflow response? I don’t really understand how that works. What even is the return type of fatalError() anyway?

@noreturn is an old concept that got replaced by Never in SE-0102. So fatalError is now just a normal () -> Never function.

If we have Never as the bottom type (meaning Never is a subtype of every type), we could replace the throw with something similar to:

// Note that this declaration compiles succesfully today.
func throwOnly(_ error: Error) throws -> Never {
  throw error
}

We can then do something like: value ?? throwOnly(error) as Never will be the subtype of Value. Currently it doesn't work as Never is unrelated to Value (except for when Value is Never).

Not that I particularly like the concept of bottom type, though.

3 Likes

I think we need to make throw an expression (currently it’s a statement) which should allow you to do a ?? throw b for example.

1 Like

We also need to figure out the type of throw. Since throwing Never (my throwOnly) is apparently not coerced properly.

Right, we would also need to give it a Never type if we decide to make Never a bottom type.

2 Likes

Curious if people agree

// requires a small library change
let url = try URL(string: location).unwrapped()  // throws Missing<URL> with FILE:LINE
let url = try URL(string: location).unwrapped(orThrows: InvalidURL(location))

and

// requires a deeper change to compiler
let url = URL(string: location) ?? throw InvalidURL(location)

Can live together? I think unwrapped() or unwrap()? is a nice API for optional.

1 Like

One minor point which I'm not sure was mentioned, if we can make x ?? throw e to work, I think it would be convenient if that expression didn't require prefixing it with try, even if the throw is technically buried inside the autoclosure expression.

1 Like

Paraphrasing freak4pc, I'm not a fan of optional ?? throw Error and strongly disagree that it "makes sense" from a language feel perspective.

This operator is currently is always like "value or value", and used like let a = wrapped ?? value or sometimes let b = wrapped ?? otherWrapped ?? value. In those cases a and b always end up being assigned values.

A throw or call to fatalError (etc) is an action not a value. Allowing Never types in ?? would then permit it to mean "value otherwise action" sometimes. To me, this is a significant change to the mental model of this operator. Changes it negatively by making it less clear and more blurry, which distracts when reading (which sense of ?? is this?), and detracts from learning (which actions can go after ?? and why).

EDITS: completed after accidentally submitting mid-sentence :^/, corrected some unquoted code, added final sentence justifying my argument, reworded this list of edits.

1 Like

As pointed out by others (such as Nevin, here), ?? can already mean “value otherwise action”, if you write a throwing expression with the appropriate type on the right-hand side of ??. I've used a function like Nevin's throwError in my projects in exactly this way.

Changing throw to be an expression simply standardizes the spelling of that throwing expression. I don't know that doing so requires making Never a bottom type, as opposed to just special casing the type of a throw expression.

7 Likes

I don't think I have had a project where I haven't needed ?? {throw MyError.errorType}()

I fully agree with @Nevin's spelling of making throw act like a bottom type.

I would also like to see throws? added to the language, which would act exactly like throws except instead of requiring try, the compiler would add an implicit try? before any call to the function.

func myThrowingFunc() throws? -> Int {
    //Do stuff
    if problem {
        throw MyError.errorType
    }
    return answer
}

This would say that there is throwing information, but you usually don't care about it

let x:Int? = myThrowingFunc() ///This has an implicit try? in front of it

But in cases where you actually do want it to throw, you would just call with the try keyword, and it will use that instead of try?

let x:Int = try myThrowingFunc()

The end result is that you can provide throwing for those who want it, without burdening the API with it when it isn't useful most of the time.

As an example, I have a packrat parser where I use the throwing information while developing/debugging a parser ruleset, but in actual production I always use try? because it lets me use if let, which simplifies the algorithm quite a bit from do catch, and the reason for the error doesn't matter... just that it failed.

1 Like

I'd also like to see throws! which is the same, but with trapping.

It would be great to have a way to say "I know you normally trap, but in this case, if you would trap, just give me a nil result or throw instead"...

2 Likes

I’m not sure I understand what you mean, but the fact that you’re saying that something with throws! could return nil without crashing makes me inclined to dislike it, as it seems to run counter to the way the bang works for Optional. If I am misunderstanding, could you maybe give an example of usage?

Overall, I think throws? could be useful (like in the case you mentioned previously) but at the same time I wonder if it might be a siren’s call for making poor design choices in cases that should normally throw.

throws! would be just like throws? except it has an implicit try! in front of any call which isn't otherwise marked with try. Thus it will trap if you call it normally:

func myTrappingFunc() throws! -> Int {
    guard !problem else {throw MyError.errorType}
    return answer
}

let x = myTrappingFunc() ///This has an implicit try! and will trap if it throws

But if you want it to throw instead, you would put try in front of it:

let x = try myTrappingFunc() ///Now it will throw instead of trap

Or if you wanted it to return an optional, and return nil when it throws:

let x = try? myTrappingFunc() ///Now it returns an optional instead

So both throws? and throws! work just like throws, but have a default which allows them to act like the other types of error handling by default. You can override that default behavior by explicitly marking it.

As I understand it, making calls to a throwing function look different to calls to a non-throwing function was an intentional design point. This suggestion completely defeats that. Plus the return type of such functions is partially a lie; it jumps between to MyReturnType when you prepend "try" or MyReturnType? without it. Transforming exception handling to nil return via a decision at the caller's side was also a design point.

4 Likes

The desire to treat throw X as an expression dovetails with other calls to make if and switch also be expressions, and definitely has my support.