$30 off During Our Annual Pro Sale. View Details »

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/

Yasuhiro Inami

June 28, 2015
Tweet

More Decks by Yasuhiro Inami

Other Decks in Programming

Transcript

  1. Swift 2 Error Handling
    V.S. Result
    2015/06/27 Swift 2 (& LLDB) Symposium
    Yasuhiro Inami / @inamiy

    View Slide

  2. Who?
    • Yasuhiro Inami / @inamiy
    • LINE Corp
    • WWDC 2015 Attendee
    • guard case .Left(let Him)
    = photo else {

    // It’s me

    throw stone 

    }

    View Slide

  3. Good old days…

    View Slide

  4. Objective-C

    View Slide

  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

    View Slide

  6. error:NULL];

    View Slide

  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

    View Slide

  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
    }];

    View Slide

  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

    View Slide

  10. Swift

    View Slide

  11. Swift 1 (Either/Result)
    • Optional + Error
    • map, flatMap … functional
    • 3rd party: LlamaKit/LlamaKit → antitypical/Result
    enum Result {
    case Success(T)
    case Failure(Error)
    }

    View Slide

  12. Sync + Result
    func doSync(arg: A) -> Result
    ...
    let result = doSync(arg)
    switch result {
    case .Success(let value):
    // do success handling
    case .Failure(let error):
    // do error handling
    }

    View Slide

  13. Async + Result
    func doAsync(arg: A,
    callback: (Result -> Void))
    ...
    doAsync(arg) { result in
    switch result {
    case .Success(let value):
    // do success handling
    case .Failure(let error):
    // do error handling
    }
    }

    View Slide

  14. We were
    pretty happy
    with Result,
    until …

    View Slide

  15. View Slide

  16. Swift 2
    Error Handling

    View Slide

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

    View Slide

  18. do-try-catch

    View Slide

  19. func doSomething(arg: A) throws
    func doSomethingElse
    (f: A throws -> B) rethrows

    View Slide

  20. throws / rethrows

    View Slide

  21. Java’s
    Exception Handling
    Model

    View Slide

  22. View Slide

  23. View Slide

  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)

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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”

    View Slide

  29. Comparison

    View Slide

  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
    }

    View Slide

  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...

    View Slide

  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...

    View Slide

  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
    }

    View Slide

  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

    View Slide

  35. Swift 2 Error Handling
    LGTM

    View Slide

  36. Really?

    View Slide

  37. Issues in Throwing Function
    • Too loose ErrorType
    • Async Issue
    • Separation of Concerns

    View Slide

  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
    }
    }

    View Slide

  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

    View Slide

  40. do {
    try throwLikeCrazy(x)
    }
    catch {
    // do error handling
    }
    Too loose ErrorType
    Catch… What???

    View Slide

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

    View Slide

  42. Async Issue
    func doSync(arg: A) throws -> R
    Sync pattern
    func doAsync(arg: A,
    callback: R -> Void) throws
    Async pattern?
    Can’t throw
    asynchronously

    View Slide

  43. Throwing Function
    execution block
    +
    return value, or
    throw error
    =

    View Slide

  44. all at once

    View Slide

  45. Throwing Function
    only works
    synchronously

    View Slide

  46. Workaround for
    asynchrony

    View Slide

  47. Separation of
    Concerns

    View Slide

  48. execution block
    +
    return value, or
    throw error

    View Slide

  49. execution
    evaluation

    View Slide

  50. Just like Result

    View Slide

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

    View Slide

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

    2-step call
    for lazy evaluation

    View Slide

  53. func doSync(arg: A)() throws -> R
    func doSync(arg: A)
    -> (Void throws -> R)
    =

    View Slide

  54. Essentially...
    Void throws -> T
    Result

    View Slide

  55. Sync + Lazy Throwing
    let result = doSync(arg)
    do {
    let value = try result()
    // do success handling
    }
    catch {
    // do error handling
    }

    View Slide

  56. try result
    rather than
    try execution

    View Slide

  57. Async + Lazy Throwing
    doAsync(arg) { result in
    do {
    let value = try result()
    // do success handling
    }
    catch {
    // do error handling
    }
    }

    View Slide

  58. Lazy Throwing
    LGTM

    View Slide

  59. Really?

    View Slide

  60. Issues in Lazy Throwing
    • try result() is ugly
    • No `()`, please…
    • Ideally, it’s better if we can try Result 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…)

    View Slide

  61. … Shouldn’t we
    just go back to
    Result?

    View Slide

  62. Result
    LGTM
    I changed
    my mind

    View Slide

  63. Really?

    View Slide

  64. Issues in Result
    • Result 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

    View Slide

  65. SO…
    WHAT IS THE BEST SOLUTION?

    View Slide

  66. Rust

    View Slide

  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(());
    }

    View Slide

  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(())
    }

    View Slide

  69. If rewrite in Swift…

    View Slide

  70. func doSomething() -> Result {
    let result: Result = ...
    let value = try result

    View Slide

  71. func doSomething() -> Result {
    let result: Result = ...
    let value = try result
    let value: T1
    switch result {
    case .Success(let v1):
    value = v1
    case .Failure(let e1):
    return .Failure(E2.convertFrom(e1))
    }

    View Slide

  72. func doSomething() -> Result {
    let result: Result = ...
    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!

    View Slide

  73. Rust uses Result
    + try result

    View Slide

  74. Much cleverer &
    type safe than throw

    View Slide

  75. Recap

    View Slide

  76. Recap
    • Both throwing function and Result
    have pros and cons
    • “try result” is more important than ”try
    execution”
    • Rust’s Error Handling is awesome

    View Slide

  77. Q. Will we be able to
    ”try result”
    in future Swift?

    View Slide

  78. Let’s dream!

    View Slide

  79. Thank you!

    View Slide