[Pitch] Optional.orThrow

You don't need new overloads to make use of ?? for this in Swift today. It's enough to provide this one extension to Optional:

extension Optional {
    static func `throw`(_ error: some Error) throws -> Self {
        throw error
    }
}

struct NumberError: Error {}

func demo(input: String) throws {
    let int = try Int(input) ?? .throw(NumberError())
}
3 Likes

Function name throw doesn't looks quite clear from the call site:

let value = value.throw(AppError.unexpectedNilValue)

It is felt like value-instance has throw(_:) method that unconditionally throws an error.
There is nothing about unwrapping of optional value or conditional flow.

However I'm not a strong proponent of method declared in Optional extension. I'm fine with it, but I also believe we should pick a better name for such function if we go this way.

Besides I think we should look for more convenient and short ways to express the idea.

Some metrics form my current project:

  • nil coalescing operator is used 569 times
  • unwrapOrThrow is used 52 times (not so much at a first look)

I'm sure some kind of unwrapOrThrow syntax will be used more often because of the following reasons:

  • adoption of async / await will make the Result<> an order of magnitude less used. In my current codebase result is used 284 times in completion handlers, all of them will be migrated to async.
  • adoption of typed throws will also decrease Result<> usage. There are ~237 DTOs having methods for conversion to domain models + ~60 other methods using the Result<> due to lack of typed throws.
    Nevertheless, Result<> will also be broadly used along the codebase, e.g. to store or transfer some data that is wrapped by Result<>.

Thus looking at my project it is clear for me that async / await with combination of typed throws will significantly increase the number of throwable functions. Many of these throwable functions need to unwrap optional values.
Because of that I have a strong feeling that we need something better than method declared in Optional extension.

I have come up with idea to explore Swift open source projects for analysis while writing this post.

[edited]

That's why there's ?? part in @pyrtsa example.

What does feel off – both "try" and "throw" are used.. normally it's either one or another:

try expression() // no "visible" throw here
...
throw error      // no try here

I'm sorry I think you misread my snippet. I wasn't adding an instance member to Optional but a static func.

1 Like

You are right, thanks for pointing this out.

There is also a related aspect to discuss: should we have a default Error type and an overload for those cases when someone doesn't care about error Type or does't want to create own error?

extension Optional {
  // allows to specify custom error Type
  public func unwrapOrThrow<E: Error>(_ errorExpression: @autoclosure () -> E) throws(E) -> Wrapped {
    guard let wrapped = self else { throw errorExpression() }
    return wrapped
}

  // will throw default error
  public func unwrapOrThrow() throws(some Error) -> Wrapped {
    guard let wrapped = self else { throw OptionalUnwrappingFailureError() }
    return wrapped
  }
}

struct  OptionalUnwrappingFailureError: Error {
  private let errorDescription: String?
  private let typeOfWrapped: Any.Type
}

If yes, should we add file: StaticString = #fileID, line: UInt = #line arguments to unwrapOrThrow() throws(some Error) function. This might be helpful in scenarios when error is propagated to upstream layers and finally logged.

I like the Perl naming in this case:

try x ?? die(SomeError.error)

where die is accompanied by a few friends like warn, carp, and croak, which display messages in different formats, and may or may not end execution.

My favourite alternative of making a "crash safe version of !" (which is essentially what this pitch is about) is unchecked exceptions. Benefits:

  • as with x^ alternative mentioned above, it will support L-value expressions. This is in sharp contrast to anything discussed before in this of previous related pitches.

  • compared to x^ mentioned above will not require a language / compiler change (only the standard library change).

  • will require a minimal amount of source changes (textually it would still look like x! just behave slightly differently if embedded within dynamicTry/catch brackets directly or indirectly. Note that this is a strict opt-in (if you don't "activate" this feature by using dynamicTry/catch there won't be any behavioural changes).

  • could be used for other things later on (array subscripts, etc)

1 Like

I think what a lot of the "I want this to look like postfix !" posts miss from the original, is that actually, the nature of the thrown error is important for many use cases. For example, the original builder pattern example:

struct XXXBuilder {
    var a: A?
    var b: B?
    var c: C?

    func build() throws(XXXBuilderError) -> XXX {
        try XXX(
            a: a.orThrow(XXXBuilderError.missingA),
            b: b.orThrow(XXXBuilderError.missingB),
            c: c.orThrow(XXXBuilderError.missingC)
        )
    }
}

Simply loses too much information if done with something that throws a fixed error type:

struct XXXBuilder {
    var a: A?
    var b: B?
    var c: C?

    func build() throws(NilOptionalError) -> XXX {
        try XXX(
            a: a.unwrapped,
            b: b.unwrapped,
            c: c.unwrapped
        )
    }
}

The information about which field is missing has been lost, and can't be reconstructed without introducing syntax just as verbose as the original.

So whilst I can certainly see having an overload with a default error, to make it more convenient in these cases, I think the version where the error is specified is still critical.

2 Likes

IMHO throw as an expression will be more suited for Swift and that's what should be brought up. Until that using Result type, simple guard, closure or some helper function feels like enough. Remember there was also discussion about doing guards like

guard let a else throw Error

which could be an option, but also matter of preference though.

Btw

builder feels a bit more javaish, when you can directly use init

struct XXX {
    let a: A
    let b: B
    let c: C

    init(a: A?, b: B?, c: C?) throws(NilOptionalError) {
        // whatever checking you prefer here
    }
}

but also matter of preference of course. :slightly_smiling_face: (I do get that builder could be used from different places).

2 Likes

Error information could be preserved...

Using your code with unchecked exceptions
typealias A = Int
typealias B = Int
typealias C = Int

struct XXX {
    var a: A
    var b: B
    var c: C
}

struct XXXBuilder {
    var a: A?
    var b: B?
    var c: C?

    func build() -> XXX {
        XXX(
            a: a.unwrap(),
            b: b.unwrap(), // line 22
            c: c.unwrap()
        )
    }
}

let xxx = dynamicTry {
    XXXBuilder(a: 1, b: nil, c: 3).build()
} catch: { error in
    print(error)
}
print(xxx)

produces this output:

correctly pinpointing the nature of the error ("OptionalUnwrapError.unwrappingNil") and its exact location ("App/main.swift", 22):

        XXX(
            a: a.unwrap(),
            b: b.unwrap(), // line 22
            c: c.unwrap()
        )

It is true that if we "upgrade" the vanilla "!" to use unchecked exceptions machinery and don't make any changes to the language itself you won't be able adding a descriptive comment about the error:

let y = x!        // no comment / custom error here
x!.mutatingFunc() // βœ… L-value usage possible

let y = x.unwrap(comment: "x")  // βœ… comment / custom error possible
x.unwrap(comment: "x").mutatingFunc() // πŸ›‘ L-value usage impossible

If both L-value usage and a comment (or a customised error) is needed we'd need a language change, e.g.:

let y = x!                      // βœ…
let z = x!(comment: "x")        // βœ…
x!.mutatingFunc()               // βœ…
x!(comment: "x").mutatingFunc() // βœ…

Correction / Addition:

We'd be able abusing subscripts to add a descriptive comment / customised error:

let y = x!                      // βœ…
let z = x[]                     // βœ…
let t = x[comment: "x"]         // βœ…
x!.mutatingFunc()               // βœ…
y[].mutatingFunc()              // βœ…
t[comment: "x"].mutatingFunc()  // βœ…
The code sketch
protocol HasDefaultValue {
    static var defaultValue: Self { get }
}
extension HasDefaultValue where Self: Inittable {
    static var defaultValue: Self { .init() }
}
extension Optional where Wrapped: HasDefaultValue {
    // TODO: make some parameters autoclosures for efficiency
    subscript(comment comment: String = "", file: String = #fileID, line: Int = #line) -> Wrapped {
        get {
            guard let self else {
                print("record error, file: \(file), line: \(line), comment: \(comment)")
                return Wrapped.defaultValue
            }
            return self
        }
        set {
            self = newValue
        }
    }
}
struct SS: Inittable, HasDefaultValue {
    mutating func mutatingFunc() {}
}
var xx: SS?
xx[comment: ""].mutatingFunc()

Is it possible using a "subscript" approach with normal try/catch instead of the above unchecked exceptions? To some extent only:

  • only for the R-value use cases (throwing subscripts are limited to be get-only at the moment).

  • or if this restriction is lifted and we have get/set throwing subscripts in the language.


As with pretty much any feature there are multiple aspects at hand here, and multiple interpretations of what should be "a goal" and "non goal". Let me write a few:

  1. the feature should not require language / compiler change
  2. the feature shall use the standard try / catch mechanism
  3. the feature should allow adding a descriptive comment (or allow to specify a customised error)
  4. the feature shall allow identifying the error source
  5. the feature should be a "safe" version of "!" usable in all contexts (including L-value).

I noticed that other people value #1 most and #5 least, and personally I'd choose the priorities the opposite way around: #5 on my book is the most important and #2 & #3 (and even #1 if needed) could be sacrificed.

OK, so here's an extension of the proposal in the original post, which satisfies all 5:

public extension Optional {
    func unwrap<E: Error>(
        orThrow error: @autoclosure () -> E
    ) throws(E) -> Wrapped {
        guard let wrapped = self else {
            throw error()
        }
        return wrapped
    }

    struct NilOptionalError: Error {
        public init() {}
    }

    var unwrapped: Wrapped {
        get throws {
            unwrap(orThrow: NilOptionalError())
        }
        set {
            self = newValue
        }
    } 
}

About 4 of 5 bullet points above are broken!

  • For starters, it doesn't even compile...

If we are removing the setter (then we might as well remove the var unwrapped as well) – we are losing #5. If we removing "throws" we are losing #2

  • file/line/comment/or custom error information would be impossible to add to "var unwrapped" (breaking #3 and #4)

  • behaviour of x! = value would be different to try x.unwrapped = value (if it did compile, I mean). I didn't put that into a bullet point but it would be reasonable to expect "when ! did trap – this would throw" behaviour.

  • try x.unwrapped.mutatingFunc() would be impossible to have without dropping #1, breaking #2 and or #5.

I did not expect that!

swift-evolution/proposals/0310-effectful-readonly-properties.md at main Β· swiftlang/swift-evolution Β· GitHub explains that settable throwing properties were omitted only due to complexity, not for any fundamental reason. Still, obviously that would mean a compiler change to go this way.

Personally, if we're admitting that this feature requires a compiler change, allowing throw E as an expression that can take on any type seems the right one. (I absolutely do not care about the "default error" or "lvalue" use-cases, though I certainly understand that others do)

Right. Just when we throw "compiler changes" into the mix it opens up questions like "could we compromise less" and "if there were no limits what would the ideal syntax look like and the ideal behaviour be". I doubt that having an incoherent two-face answer of a get-only file/line/comment/error customisable unwrap() on one hand and a rudimentary in those regards but writeable unwrapped on another (different syntax, different features, different rules, no support of x! = value behaviour) makes an ideal feature compared to the syntactic simplicity of ! (built-in pseudo) operator.

After discovering this thread, I decided I would add a similar extension to my projects. But to me the most consistent naming would be this:

extension Optional {
   func get(orThrow error: @autoclosure () -> some Error) throws -> Wrapped {
      guard let unwrapped = self else { throw error() }
      return unwrapped
   }
}

Why? Like I've explained in the collapsible disclosure section here, an Optional is nothing else but a Result where the failure case .none has no additional information about the reason it is .none. In other words, an Optional<T> type is syntactic sugar for Result<T, VoidError>.

And what API does the Result type already contain that unwraps the success case or throws an error? Yes, it's the get function. The only difference to that function is that in the case of Optional the error needs to be passed as a parameter as it's not included int he Optional type, so we have to pass orThrow:.

The result is an API call site that reads like this:

someOptional.get(orThrow: MyError.unexpectedlyFoundNil)
2 Likes

get() is an aberration added in a single round of the Result review with no external discussion (it was the second or third review at that point), so it shouldn't be used as justification for further naming. We can see that reflected in Task, which was going to originally use get() to access its produced value, but went with value after async property accessors were added (specifically to get that superior naming).

If Result.get() were named today it would likely be similar:

var value: Success {
  get throws(Failure) {
    // .success
  }
}

Even if none of that were true, get() just isn't a good name, and there doesn't seem to be a compelling reason to align Optional and Result here in the first place. Personally I think unwrapped(orThrow:), with a default error provided, is the clearest and most ergonomic version. I'd be happy with a version that can't throw a custom error too.

6 Likes

I summarised options currently available via normal language means:

Neither option allows ticking all the boxes.