[swift-evolution] Proposal: Allow Type Annotations on Throws

Matthew Johnson matthew at anandabits.com
Tue Dec 22 11:50:24 CST 2015


> On Dec 21, 2015, at 3:00 PM, David Owens II <david at owensd.io> wrote:
> 
> 
>> I understand that Rust is not doing implicit conversions, but the effect for the user is pretty much the same.  The try macro is converting the underlying error to the type that can be propagated.  As I stated, Swift is not Rust and deserves a different solution.  
>> 
>> Nevertheless, that does not minimize the need to solve the problem.  I maintain that the problem solved by the try macro is a significant one that is not addressed by the current proposal.  I would really like to see it addressed one way or another.
>> 
>>> 
>>> You could make it “nicer” by doing something like this:
>>> 
>>> try MyError.convertFrom(try funcThatThrowsAnErrorThatMustBeTranslatedItoMyPublishedError())
>> 
>> Can you elaborate on how you think this would work?  If funcThatThrowsAnErrorThatMustBeTranslatedItoMyPublishedError actually throws it will be propagated to the next enclosing catch clause.  MyError.convertFrom will not have a chance to do anything with it.
> 
> Here’s a full playground example (I’ve annotated in comments where the type of error could be described):
> 
> enum InternalError: ErrorType {
>     case Internal(value: Int)
> }
> 
> enum PublishedError: ErrorType {
>     static func from<T>(@autoclosure fn: () throws -> T) throws -> T {
>         do {
>             return try fn()
>         }
>         catch InternalError.Internal(let value) {
>             throw PublishedError.Converted(value: value)
>         }
>         catch {
>             fatalError("unsupported conversion")
>         }
>     }
>     
>     case Converted(value: Int)
> }
> 
> 
> func example() {
> 
>     func bad(value: Int) throws /* InternalError */ -> Int {
>         if value % 2 == 0 { throw InternalError.Internal(value: value) }
>         return value
>     }
> 
>     func verbose(value: Int) throws /* PublishedError */ -> Int {
>         do {
>             return try bad(value)
>         }
>         catch InternalError.Internal(let value) {
>             throw PublishedError.Converted(value: value)
>         }
>         catch {
>             fatalError("unsupported conversion")
>         }
>     }
>     
>     func convert(value: Int) throws /* PublishedError */ -> Int {
>         return try PublishedError.from(try bad(value))
>     }
>     
>     do {
>         let r1 = try verbose(11)
>         print("verbose: \(r1)")
>         
>         let r2 = try convert(9)
>         print("converted: \(r2)")
>     }
>     catch {
>         print("error: \(error)")
>     }
> 
> }
> 
> example()
> 
> 
> As you can see, the “verbose()” and the “from()” conversion are basically the same implementation. What I’m saying is that I believe you can simply do the explicit conversion yourself without much fanfare (compare the verbose() and convert() implementations).
> 
> In the implementation of PublishedError.from() you can use Swift’s pattern matching to do all of your conversions in a single place. Note that where the implementation of “from” is at doesn’t matter, it could be on another type or a free function, whatever.

That is a pretty clever use of @autoclosure!  It can be made even be made even more concise with typed errors and a top level conversion function:

@protocol ErrorTypeConvertible {
	// implementations will have to include a default clause which is either going to call fatalError 
        // or be an ‘UnknownError’ case in the enum
	init(underlyingError: ErrorType) { … }
        // or
	init<E: ErrorType>(underlyingError: E) { … } 
}

func from<T/*, Internal, Published: ErrorTypeConvertible*/>(@autoclosure fn: () throws /* Internal */ -> T) throws /* Published */ -> T {
    do {
        return try fn()
    }
    catch let error as Internal {
        return Published(underlyingError: error)
    }
   // hopefully the compiler is able to detect that this is 
}

    func convert(value: Int) throws /* PublishedError */ -> Int {
        return try from(try bad(value))
    }

This addresses my largest concern which is cluttering up the control flow.  The additional noise of an extra ‘try' and a call to ‘from’ isn’t great, but it is tolerable, at least initially (I think we would eventually learn that it is just noise and get rid of it).  

Unfortunately, I don’t see a way to make it safe.  You had to use fatalError in a default case to make it work.  An alternative would have been to include an ‘UnknownError’ case in ‘PublishedError’.  Neither is not an acceptable solution IMO.

If you can make PublishedError.from safe without requiring an ‘UnknownError’ case it will also be possible to make a top-level ‘from’ safe.  That would be acceptable, but I don’t believe it’s possible in the current language and I’m not aware of any proposed changes that would make it possible.

This top level `from` example also brings up a couple of points that I don’t recall being addressed in your proposal.  Specifically, the interaction of typed errors with generics and type inferrence.  The obvious thing to do is allow them to behave the same as any other part of the return type.  If that is what you expect and it is not already stated, you should update the proposal to specify that.  If you expect something different you definitely need to specify what that is.  An implementer will need to know how this should be handled.

I still consider this to be an unresolved concern.  I would like to have a safe way to perform error conversion during propagation without cluttering up my control flow and seriously degrading readability.  This is a problem that can and has been solved in other languages.  IMO it is should be considered an essential element of a proposal introducing typed errors.

Matthew

> 
>> Are you willing to explore adding *explicit* syntax to convert thrown errors to your proposal?  That seems like it might be a reasonable compromise between implicit conversions and manual boilerplate.  
> 
> The only boiler plate I’m seeing is the explicit conversion call: PublishedError.from(try bad(value))
> 
> Am I misunderstanding something? 
> 
> To me, this would be much more confusing:
> 
>     func convert(value: Int) throws /* PublishedError */ -> Int {
>         return try bad(value)     /* implicit conversion from InternalError -> PublishedError */
>     }
> 
> If there were implicit type conversions, this would have to be something that Swift supported all up. I’d be very hesitant to make this work for only errors. For example, how does implicit conversion work if we can later extend this to async behaviors? Do we have special conversions that can take an async error make it a synchronous error? How about vice-versa?
> 
> I guess I wouldn’t want to go further than having explicit conversions until we better understood all of those answers and how implicit type conversion would work in Swift generally. If I recall, Swift had implicit type conversion in the early versions, and it has been removed in most places. 
> 
> -David
> 

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151222/d7006ad4/attachment.html>


More information about the swift-evolution mailing list