I find myself wanting to define an operation that converts failable initializers to throwing ones. When I have a throwing initializer contains multiple failing initializers, I think it improves the clarity and readability of the code. (see below)
UPDATE: Leaving the original implementation but I am not opposed to changing the spelling or making this an operator or whatever. Lots of great ideas and info below.
This is one of those additions that's so easy to add and just syntax sugar so I imagine gets deferred indefinitely.
However, I think not having it actively encourages less robust error handling. This comes in the form of either using bogus nil coalesced default values or using compactMap where it shouldn't be. It's a very small change that encourages better error handling.
Yes, I've used a form of this in every project I've been on in the last 4 years. It's extremely useful in throwing contexts. I think the biggest issue (aside from a lack of interest in the core team) would be naming and the exact signature. Swift doesn't have a precedent for or* methods. The closest existing API would be get() on Result, but that method name is horrible.
I'll note as the person who originally pushed for try? that I regret adding it. The most common use is to ignore errors, and that should have been a free function ignoringErrors(_:) (like withoutActuallyEscaping(_:do:)). Throwing away an error without logging it or anything is poor practice and should be made more explicit when it is the right thing to do.
(But I do think that either ?? throw or a method on Optional makes sense. That's adding information, not dropping it.)
In my private extensions to the standard library, I have the "unwrap or die" operator (!!), but I also have the "unwrap or throw" operator too:
let maybeValue: Optional<Foo> = ...
let crashIfNil = maybeValue !! "Logic dictates this should never be nil"
let throwIfNil = try maybeValue ?! MyError.missingValue
I've found both to be incredibly useful and I'm sad the pitch wasn't accepted.
This is the implementation:
infix operator !!: NilCoalescingPrecedence
infix operator ?!: NilCoalescingPrecedence
public func !!<T>(value: T?, error: @autoclosure () -> String) -> T {
if let value = value { return value }
fatalError(error())
}
public func ?!<T>(value: T?, error: @autoclosure () -> Error) throws -> T {
if let value = value { return value }
throw error()
}
Well, even if you log something, who cares if no one is reading those logs in production? Not every developer can afford remote error reporting (*).
Last time I saw try?, it was in order to gracefully ignore some Metal/SceneKit errors, and just disable some unavailable rendering features. Whenever this information gets any value, the error won't be thrown away. Until then, try? was just the perfect tool, when placed at the correct location. Got an error creating the ambiant occlusion renderer? OK, let's just ignore ambiant occlusion.
(*) Whenever Apple is able to report crashes correctly, they may eventually report regular errors as well? This is a very valuable feature of Crashlytics.
I like it. Maybe the error could have some type information about the Wrapped type. It would be nice not have to define it every time I opened a new playground.
I agree with the op this is a long-standing "issue" that I always look at and think, "Uhm, wasn't this solved yet?".
I'm a fan of optional ?? throw Error and I think it "makes sense" from a language feel perspective. This is sort of what Kotlin lets you do as well, so there is already some precedence in other modern languages. The trick here would be to understand how to fix the type constraints or specialize that "Never" case (e.g. think optional ?? fatalError())
There is an opportunity to make this a wider proposal to allow sort of a "bottom type" on the right-side of nil-coalescing, and I believe it would be extremely readable and fit the spirit of Swift.
I'm not a huge an of using operators for these sort of things (or most things, actually) given how opinionated those are. They fit well in some team's code base but not all teams' codebases IMO.
I would prefer not having an orThrow(_:) (or same spirit but with different name) but it is a solid option if allowing a more robust solution is out of the question for the Core Team.
It seems as if others think they always have an error ready to go. I can't get onboard with that idea. This shouldn't go through if we don't have a default way to convert nil to an error.
XCTest uses UnwrappingOptional for XCTUnwrap. Maybe we should deprecate that, and XCTUnwrap along with it, once there's a type available everywhere.
It either has to be a method, or two operators. (Operators with defaults compile, but you can't actually use them. )
infix operator ¿¿
postfix operator ¿¿
public func ¿¿ <Wrapped>(
wrapped: Wrapped?,
error: @autoclosure () -> Error
= Wrapped?.UnwrapError() // This can't be used.
) throws -> Wrapped {
if let wrapped = wrapped {
return wrapped
} else {
throw error()
}
}
public postfix func ¿¿ <Wrapped>(wrapped: Wrapped?) throws -> Wrapped {
try wrapped ¿¿ Wrapped?.UnwrapError()
}
I'd love to be able to define the error, nested in these bodies, but the compiler won't allow it yet. So a type either needs to be added to go along with this, or chosen. I don't think there's a good existing candidate for the latter, hence why they made one up for XCTUnwrap?
I like the general principle that operators should be backed by methods. Methods are more flexible but more verbose, while operators are less clear but less verbose. Having both gives you maximum flexibility and choice.
And I agree, there should be a default OptionalError or something that will be the default Error.
extension Optional {
struct Missing : Error, CustomStringConvertible {
let message: String?
var description: String {
let base = "Missing expected value of type \(Wrapped.self)"
if let message = self.message {
return "\(base): '\(message)'"
} else {
return base
}
}
}
}
func unwrap<T>(_ value: T?, _ message: String? = nil) throws -> T {
guard let value = value else {
throw Optional<T>.Missing(message: message)
}
return value
}
Nesting the Missing error inside Optional makes the Missing type generic. If you want to catch the unexpected-nil error for some reason, you have to know the Wrapped type of the Optional that threw it:
do { ... }
catch let error as Optional<String>.Missing {
// won't catch Optional<Int>.Missing
}
I'm not sure when it will be useful to catch all unexpected-nil errors, or to catch an unexpected-nil error for any specific Wrapped type. But I'd rather it be possible to catch them all (I wanna be the very best, like no one ever was…), or to catch them just for specific types.
We can make that possible by passing along the Wrapped type in a property instead of as a generic argument:
public enum AnyOptional { // Or make it a struct, if that's useful.
public class UnwrapError: Error {
public init() {
public extension Optional {
final class UnwrapError: AnyOptional.UnwrapError {
Examples of stuff I use that looks like that, erasing a container of some other sort, including a use case for this pitch:
public enum AnyCaseIterable<Case> {
public enum AllCasesError: Error {
/// No `AllCases.Index` corresponds to this case.
case noIndex(Case)
}
}
public extension CaseIterable where Self: Equatable {
/// The first match for this case in `allCases`.
/// - Throws: `AnyCaseIterable<Self>.AllCasesError.noIndex`
func caseIndex() throws -> AllCases.Index {
try Self.allCases.firstIndex(of: self).unwrap(
orThrow: AnyCaseIterable.AllCasesError.noIndex(self)
)
}
}
public extension AnyCollection {
/// Thrown when `element(at:)` is called with an invalid index.
struct IndexingError: Error { }
}
public extension Collection {
/// - Returns: same as subscript, if index is in bounds
/// - Throws: `AnyCollection<Element>.IndexingError`
func element(at index: Index) throws -> Element {
guard indices.contains(index)
else { throw AnyCollection<Element>.IndexingError() }
return self[index]
}
}
How about the following for a default Error? I've included the original that accepts an Error to throw as well. Both seem useful to me.
extension Optional {
struct MissingValueError: Error, CustomStringConvertible {
let file: StaticString
let line: UInt
var description: String {
"Expected a value of type \(Wrapped.self) at \(file)::\(line)"
}
}
func orThrow(_ error: @autoclosure () -> Error) throws -> Wrapped {
if let wrapped = self {
return wrapped
} else {
throw error()
}
}
func orThrow(file: StaticString = #file, line: UInt = #line) throws -> Wrapped {
if let wrapped = self {
return wrapped
} else {
throw MissingValueError(file: file, line: line)
}
}
}
let x: String? = nil
try x.orThrow()
Outputs this in my playground:
Playground execution terminated: An error was thrown and was not caught: ▿ Expected a value of type String at MyPlayground.playground::31 - file : "MyPlayground.playground" - line : 31