[swift-evolution] [Pitch] Revamp Optional and Throws

Xiaodi Wu xiaodi.wu at gmail.com
Sun Apr 30 17:44:34 CDT 2017


On Sun, Apr 30, 2017 at 5:05 PM, Gor Gyolchanyan <gor at gyolchanyan.com>
wrote:

>
> On May 1, 2017, at 12:10 AM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
>
> You may wish to read the rationale behind the current error handling
> design:
>
> https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst
>
> A Result type like you suggest has been considered and rejected in favor
> of the current design. Briefly, optionals and throwing errors are distinct
> because they are considered superior ways for handling distinct types of
> error.
>
> In the case of a simple domain error, there is only one way to fail;
> therefore, optional return values are considered the best way to model that
> error.
>
> In the case of a recoverable error, the document above describes why
> marked propagation (the current implementation in Swift) is considered
> superior to typed propagation (your suggestion).
>
>
> My proposal is not about replacing Optionals and throwing functions with a
> Result type, it’s about separating the representation of an error from its
> propagation.
> Optionals and throwing functions solve two different problems, but they
> are not dealing with two different types of error.
>

The basic premise of Swift error handling design is that there exist
different types of error. From the document:

> What is an error? There may be many different possible error conditions
in a program, but they can be categorized into several kinds based on how
programmers should be expected to react to them. Since the programmer is
expected to react differently, and since the language is the tool of the
programmer's reaction, it makes sense for each group to be treated
differently in the language.

Optionals are for storing and representing a value that might not exist
> (most commonly due to an unambiguous error).
> Error handling is for propagating an error.
> Returning an optional is essentially the same as returning a non-optional
> and throwing a dedicated “something went wrong” error, because due to the
> optional unwrapping mechanic, you cannot avoid dealing with the fact that
> there might have been an error. Optionals only allow you to delay the
> inevitable error handling, not avoid it. The use cases where the exact
> reason for an error is no important have nothing to do with whether or not
> that error should be available. The optional chaining, if-let statements
> and all other ways one might try to handle an optional value do not
> fundamentally require lack of error information.
> The error handling mechanism, on the other hand, does not concern itself
> with representing the error, but only propagating it. Even an optional
> value with a general-purpose .none case has different levels of importance
> in different cases. More often than not, when propagating an optional value
> to a non-optional target, you’ll be stuck with dealing with the error
> immediately, which is exactly what throwing functions force you to do.
> I suggest we enhance the current error representation and propagation
> mechanisms to be able to seamlessly handle cases where an erroneous value
> need to be stored as-is (along with its error) or unpacked and propagated
> (by throwing the error), not just representing a general “error”.
> The general use case is to be able to catch a throwing call into an enum
> that stores the value or the error and then being able to unpack it in a
> throwing context (unpack it or throw the error).
> This use case is a strict superset of the current Optional mechanic
> (catching a potentially missing value) and error handling (always manually
> throwing an error after manual checks).
> The aforementioned suggestion about how to do that is indeed faulty, as
> pointed out by Robert Widmann, but the problem is still valid, in my
> opinion.
>

I'd highly recommend taking some time to digest the existing rationale.
You're basing your argument on contradicting the fundamental premise of the
existing design, which begins with this: (a) there are different types of
error; (b) the programmer is expected to react differently to different
types of error; (c) the language is a tool to help the programmer react;
(d) optionals and errors are not unified, and unification is a non-goal,
because they are designed to help the programmer react differently to
different types of error.

Do you have a specific use case in mind that is not well accommodated by
optionals or by throwing functions? What is it? Into what category does
that use case fall, in terms of the types of error enumerated in the error
handling rationale document?

On Sun, Apr 30, 2017 at 13:51 Gor Gyolchanyan via swift-evolution <
> swift-evolution at swift.org> wrote:
>
>>
>> > On Apr 30, 2017, at 9:29 PM, Robert Widmann <devteam.codafi at gmail.com>
>> wrote:
>> >
>> >
>> >> On Apr 30, 2017, at 1:43 PM, Gor Gyolchanyan <gor at gyolchanyan.com>
>> wrote:
>> >>
>> >> It doesn’t have to be a massive source-break, since this pitch is
>> supposed to be a strict superset of what Optional and throwing is currently.
>> >> The only thing that I can think of at this moment that would break is
>> this syntax:
>> >>
>> >> let foo: Int? = .none // Error: Can’t convert (Error) -> Int? to Int?
>> >>
>> >
>> > Except it’s not a strict superset if you break every use of this case
>> as an RValue.  Especially when so much of Swift’s syntax and major patterns
>> revolve around the manipulation of optionals.
>> >
>> >> The ExpressibleByNilLiteral, the try/throw syntax, all of those things
>> would work as they are right now.
>> >> Error handling as it is currently, is essentially a hidden `error` out
>> parameter and a whole bunch of codegen.
>> >> Even the semantical changes described earlier would be purely additive.
>> >
>> > Don’t get me wrong, I think you’ve identified the problem space well, I
>> just disagree with the solution.
>>
>> Yeah, you’re right. It would take some next-level fixits to deal with the
>> consequences of changing the most fundamental data type of Swift I can
>> think of.
>> I’d really appreciate it if you’d offer an alternative solution to this
>> problem.
>> The problem, as I understand it, is as follows:
>>
>> A lot of Swift’s logic revolves around the notion that some values might
>> be missing for whatever reason and some functions might fail for whatever
>> reason.
>> Any function’s effect can be summed up as the union of its return value
>> and the global state that it changes (that includes captured closure
>> scopes).
>> This could be boiled down to the statement that “Values that a function
>> sets and returns completely express the purpose of the function”.
>> The optional gives an extremely convenient way of representing values
>> that might not exist (which, when returned from a function often means
>> “failed for an unknown reason”).
>> The fact that Optional is a type, rather then a function attribute allows
>> us to store and imperatively manipulate the outcome of logically failable
>> functions, but unfortunately, it doesn’t allow us to reason about the cause
>> of the failure.
>> On the other hand, throwing functions captures the logic of dealing with
>> specific failures very well, but does not allow us to store and manipulate
>> them easily, leaving us with workarounds like wrapping errors in enums with
>> values and re-throwing the errors on their way out of the generic pipeline.
>> I’d like to come up with a solution that would unify the optionals and
>> the throwing functions into a single mechanism for dealing with the concept
>> of failure, taking the best of both worlds and getting the benefits of the
>> new synergies.
>> This pitch was a first rough idea about the direction in which we could
>> go in trying to find a solution.
>> I chose to enhance Optional instead of introducing a new type like
>> Failable, so that we could make do with minimal language changes and
>> migration procedures.
>>
>> This problem is kinda similar to the variadic parameter problem, which
>> makes it impossible to forward calls to variadic functions simply because
>> that feature is too magical and does not provide a way to store and
>> propagate its logic.
>>
>> Another way I could think of solving it would be to allow overloading the
>> postfix `!` and `?` operators (which would currently only be defined for
>> Optionals), which would allow us to define the Failable enum type with some
>> error handling syntax integration and make it feel more at home in the
>> midst of Optionals.
>>
>> Or better yet, make an OptionalProtocol and move the current magical
>> logic to it, leaving the existing Optional perfectly intact and allowing
>> userspace implementations.
>> This would also greatly benefit numerous use cases of “invalidatable”
>> types (like file handlers that can be closed) that would no longer have to
>> either fatalError or use unwieldy wrappers that operate on Optionals.
>>
>> > ~Robert Widmann
>> >
>> >>
>> >>> On Apr 30, 2017, at 8:35 PM, Robert Widmann <devteam.codafi at gmail.com>
>> wrote:
>> >>>
>> >>> This "revamp" is isomorphic to adding a Sum type to stdlib and
>> plumbing error handling syntax through.  I'd much rather see that than the
>> massive source-break this would entail.
>> >>>
>> >>> ~Robert Widmann
>> >>>
>> >>> 2017/04/30 13:11、Gor Gyolchanyan via swift-evolution <
>> swift-evolution at swift.org> のメッセージ:
>> >>>
>> >>>> I’d like to suggest a bit of redesigning the Optional type and
>> throwing functions to provide a single powerful and flexible mechanism for
>> dealing with unexpected situations.
>> >>>>
>> >>>> In short, The Optional would have an associated value of type Error
>> added to its `none` case, which would describe the reason why the wrapped
>> value is missing.
>> >>>>
>> >>>> public enum Optional<Wrapped> {
>> >>>>
>> >>>> case .some(Wrapped)
>> >>>>
>> >>>> case .none(Error)
>> >>>>
>> >>>> }
>> >>>>
>> >>>> The Optional's ExpressibleByNilLiteral would initialize it with an
>> error that corresponds to what is currently fatalError-ed as "unexpectedly
>> found nil while unwrapping an Optional value".
>> >>>>
>> >>>> The forced unwrapping operator (postfix `!`) would behave the same
>> way as it does now, except in case of a fatal error it would print out the
>> underlying error, instead of the aforementioned hard-coded string.
>> >>>>
>> >>>> The optional chaining operator (postfix `?`) would behave the same
>> way as it does now, except when it stops evaluating and returns the
>> Optional, it would contain the error, returned by the sub-expression that
>> failed to evaluate.
>> >>>>
>> >>>> Any throwing function would be equivalent to a function that returns
>> an Optional. If the function is declared as throwing and returning an
>> Optional at the same time, it would be equivalent to a function returning
>> an Optional Optional.
>> >>>>
>> >>>> The if-let statement would bind the `let` variable to the wrapped
>> value inside the "then" block and would bind it to the error in the "else"
>> block. Chained else-if blocks would all be considered part of the
>> overarching "else" block, so all of them would be able to access the error
>> bound to the if-let name.
>> >>>>
>> >>>> The guard-let and case-let statements are essentially just rewrites
>> of if-let with some added logic.
>> >>>>
>> >>>> The `try` keyword, applied to an optional would behave like this:
>> >>>>
>> >>>> public func try<T>(_ optional: T?) throws -> T {
>> >>>> guard let wrapped = optional else {
>> >>>>     throw wrapped // Remember, if-let, guard-let and case-let
>> statements bind the let name to the error in case of a failure.
>> >>>> }
>> >>>> return wrapped
>> >>>> }
>> >>>>
>> >>>> Multiple let bindings in a single if-let statement are essentially
>> rewrites of a nested chain of if-let statements.
>> >>>>
>> >>>> The `try` keyword applied to an optional would unwrap the value or
>> throw the error.
>> >>>> The `try?` keyword applied to a throwing function call would cause
>> any thrown errors to be caught and put into the returned Optional, instead
>> of simply ignored.
>> >>>> The `try!` keyword applied to a throwing function call would behave
>> as you'd expect: just like `try?` except immediately force-unwrapped.
>> >>>>
>> >>>> A throwing function would be convertible to a non-throwing
>> optional-returning function and vice versa.
>> >>>> This would allow making use of throwing functions when dealing with
>> generics or protocols that allow arbitrary return types, without having to
>> sacrifice the convenience of error-handling logic. Conversely, it would
>> allow to write generic code that deals with any type of function without
>> having to implement special cases for throwing functions. This means that
>> the two function types would be interchangeable and one would be able to
>> satisfy protocol requirements of the other. The `rethrows` idiom would then
>> become a natural consequence of writing generic functions that may return
>> optional and non-optional results just as well.
>> >>>>
>> >>>> _______________________________________________
>> >>>> swift-evolution mailing list
>> >>>> swift-evolution at swift.org
>> >>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>> >>
>> >
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170430/1e1b2890/attachment.html>


More information about the swift-evolution mailing list