Swift 2 Error Handling vs Result<T, E>

Swift 2 Error Handling vs Result<T, E>

Swift 2 (& LLDB) シンポジウム
http://realm.connpass.com/event/16556/

Eac0bf787b5279aca5e699ece096956e?s=128

Yasuhiro Inami

June 28, 2015
Tweet

Transcript

  1. Swift 2 Error Handling V.S. Result<T, E> 2015/06/27 Swift 2

    (& LLDB) Symposium Yasuhiro Inami / @inamiy
  2. Who? • Yasuhiro Inami / @inamiy • LINE Corp •

    WWDC 2015 Attendee • guard case .Left(let Him) = photo else {
 // It’s me
 throw stone 
 }
  3. Good old days…

  4. Objective-C

  5. Error Handling in Objective-C + (id)JSONObjectWithData:(NSData *)data options: (NSJSONReadingOptions)opt error:(NSError

    **)error NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; id json = [NSJSONSerialization JSONObjectWithData:jsonData options:1 error:NULL]; Method Usage
  6. error:NULL];

  7. Passing Error Pointer (Obj-C) NSError* error = nil; id json

    = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error]; if (error) { // do error handling return } // do success handling
  8. Async Error Handling (Obj-C) NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData

    *data, NSURLResponse *response, NSError *error) { if (error) { // do error handling return } // do success handling }];
  9. or, Exception Handling (Obj-C) @try { // Code that can

    potentially throw an exception } @catch (NSException *exception) { // Handle an exception thrown in the @try block } @finally { // Code that gets executed // whether or not an exception is thrown } … but rarely used in Objective-C
  10. Swift

  11. Swift 1 (Either/Result) • Optional<T> + Error • map, flatMap

    … functional • 3rd party: LlamaKit/LlamaKit → antitypical/Result enum Result<T, Error> { case Success(T) case Failure(Error) }
  12. Sync + Result func doSync<A, T, E>(arg: A) -> Result<T,

    E> ... let result = doSync(arg) switch result { case .Success(let value): // do success handling case .Failure(let error): // do error handling }
  13. Async + Result func doAsync<A, T, E>(arg: A, callback: (Result<T,

    E> -> Void)) ... doAsync(arg) { result in switch result { case .Success(let value): // do success handling case .Failure(let error): // do error handling } }
  14. We were pretty happy with Result, until …

  15. None
  16. Swift 2 Error Handling

  17. do { try doSomething(arg) } catch { // do error

    handling }
  18. do-try-catch

  19. func doSomething<A>(arg: A) throws func doSomethingElse<A, B> (f: A throws

    -> B) rethrows
  20. throws / rethrows

  21. Java’s Exception Handling Model

  22. None
  23. None
  24. Swift 2 Error Handling • Throwing Function • Returns value,

    or throws error (ErrorType) • Must declare throws / rethrows • Supertype of non-throwing function • Works swiftly with existing Objective-C Cocoa APIs • Can set breakpoint (e.g. br s -E swift -O MyError) • Similar to Java’s Checked Exception (not NSException)
  25. Ref: Checked/Unchecked Exceptions • Checked Exception • Error from outside

    of program control & should be recoverable • Forces caller to use do-catch conditional blocks • No unwinding callstack penalties for unhandled errors • Unchecked Exception • Programmer’s mistake & should NOT be recoverable
  26. func doSomething<A>(arg: A) throws ... do { try doSomething(arg) }

    catch { // do error handling }
  27. func doSomething<A>(arg: A) throws ... do { try doSomething(arg) }

    catch { // do error handling }
  28. do-try-catch / try! • do-try-catch, not “try-catch” • try expression

    is required for **every ** call of throwing function • or, try! (forced-try) to omit do-catch blocks • catch clause can be used like switch statement • Use defer in replace of “finally”
  29. Comparison

  30. Sequential flow (Result) let result3 = doSync1(arg) .flatMap(doSync2) .flatMap(doSync3) switch

    result3 { case .Success(let value3): // do success handling case .Failure(let error): // do error handling }
  31. Sequential flow (Result) let result3 = doSync1(arg) .flatMap(doSync2) .flatMap(doSync3) switch

    result3 { case .Success(let value3): // do success handling case .Failure(let error): // do error handling } Functional All Error types must be same...
  32. Sequential flow (Result) let result3 = doSync1(arg) .flatMap(doSync2.mapError {…}) .flatMap(doSync3.mapError

    {…}) switch result3 { case .Success(let value3): // do success handling case .Failure(let error): // do error handling } Error conversion is sometimes required...
  33. Sequential flow (Throwing Func) do { let value1 = try

    doSync1(arg) let value2 = try doSync2(value1) let value3 = try doSync3(value2) // do success handling } catch { // do error handling }
  34. Sequential flow (Throwing Func) do { let value1 = try

    doSync1(arg) let value2 = try doSync2(value1) let value3 = try doSync3(value2) // do success handling } catch { // do error handling } Imperative Can catch any ErrorTypes
  35. Swift 2 Error Handling LGTM

  36. Really?

  37. Issues in Throwing Function • Too loose ErrorType • Async

    Issue • Separation of Concerns
  38. Too loose ErrorType func throwLikeCrazy(x: Int) throws { switch x

    { case 0: throw MyError.XXX case 1: throw YourError.YYY default: throw HisError.ZZZ } }
  39. func throwLikeCrazy(x: Int) throws { switch x { case 0:

    throw MyError.XXX case 1: throw YourError.YYY default: throw HisError.ZZZ } } Too loose ErrorType No ErrorType info Can throw many different ErrorTypes
  40. do { try throwLikeCrazy(x) } catch { // do error

    handling } Too loose ErrorType Catch… What???
  41. Async Issue func doSync<A, R>(arg: A) throws -> R Sync

    pattern func doAsync<A, R>(arg: A, callback: R -> Void) throws Async pattern?
  42. Async Issue func doSync<A, R>(arg: A) throws -> R Sync

    pattern func doAsync<A, R>(arg: A, callback: R -> Void) throws Async pattern? Can’t throw asynchronously
  43. Throwing Function execution block + return value, or throw error

    =
  44. all at once

  45. Throwing Function only works synchronously

  46. Workaround for asynchrony

  47. Separation of Concerns

  48. execution block + return value, or throw error

  49. execution evaluation

  50. Just like Result<T, E>

  51. func doSync<A, R>(arg: A) throws -> R func doSync<A, R>(arg:

    A)() throws -> R ↓
  52. func doSync<A, R>(arg: A) throws -> R func doSync<A, R>(arg:

    A)() throws -> R ↓ 2-step call for lazy evaluation
  53. func doSync<A, R>(arg: A)() throws -> R func doSync<A, R>(arg:

    A) -> (Void throws -> R) =
  54. Essentially... Void throws -> T Result<T, ErrorType> 㱻

  55. Sync + Lazy Throwing let result = doSync(arg) do {

    let value = try result() // do success handling } catch { // do error handling }
  56. try result rather than try execution

  57. Async + Lazy Throwing doAsync(arg) { result in do {

    let value = try result() // do success handling } catch { // do error handling } }
  58. Lazy Throwing LGTM

  59. Really?

  60. Issues in Lazy Throwing • try result() is ugly •

    No `()`, please… • Ideally, it’s better if we can try Result<T, E> enum rather than try throwing function • Too verbose for sync task & can’t throw immediately • Use lazy throwing for async only? 
 (but it will lack consistency…)
  61. … Shouldn’t we just go back to Result<T, E>?

  62. Result<T, E> LGTM I changed my mind

  63. Really?

  64. Issues in Result<T, E> • Result<T, E> is just an

    Enum • Same weight for both success value and error • No compiler support (breakpoint, do-catch) • Too easy to ignore errors • var value: T? … Result → Optional conversion • map() / flatMap() … using success value without checking error
  65. SO… WHAT IS THE BEST SOLUTION?

  66. Rust

  67. Error Handling in Rust // before fn write_info(info: &Info) ->

    io::Result<()> { let mut file = File::create("list.txt").unwrap(); if let Err(e) = writeln!(&mut file, "name: {}", info.name) { return Err(e) } if let Err(e) = writeln!(&mut file, "age: {}", info.age) { return Err(e) } return Ok(()); }
  68. Error Handling in Rust // after fn write_info(info: &Info) ->

    io::Result<()> { let mut file = try!(File::create("list.txt")); // Early return on error try!(writeln!(&mut file, "name: {}", info.name)); try!(writeln!(&mut file, "age: {}", info.age)); Ok(()) }
  69. If rewrite in Swift…

  70. func doSomething<T2, E2>() -> Result<T2, E2> { let result: Result<T1,

    E1> = ... let value = try result
  71. func doSomething<T2, E2>() -> Result<T2, E2> { let result: Result<T1,

    E1> = ... let value = try result let value: T1 switch result { case .Success(let v1): value = v1 case .Failure(let e1): return .Failure(E2.convertFrom(e1)) } 㱻
  72. func doSomething<T2, E2>() -> Result<T2, E2> { let result: Result<T1,

    E1> = ... let value = try result let value: T1 switch result { case .Success(let v1): value = v1 case .Failure(let e1): return .Failure(E2.convertFrom(e1)) } Early-exit with auto-mapping Error! 㱻
  73. Rust uses Result<T, E> + try result

  74. Much cleverer & type safe than throw

  75. Recap

  76. Recap • Both throwing function and Result<T, E> have pros

    and cons • “try result” is more important than ”try execution” • Rust’s Error Handling is awesome
  77. Q. Will we be able to ”try result” in future

    Swift?
  78. Let’s dream!

  79. Thank you!