A deep dive into the result type — Appdevcon 2019

A deep dive into the result type — Appdevcon 2019

In this presentation—given at Appdevcon march 2019—we take a closer look at Swift 5’s result type and how this functional programming paradigm compares to “regular” imperative error handling.

C83dc9aa541b7c616e1dcf2851bbe554?s=128

Tjeerd in 't Veen

March 15, 2019
Tweet

Transcript

  1. A DEEP DIVE INTO THE RESULT TYPE

  2. Tjeerd in ’t Veen @tjeerdintveen
 www.swiftindepth.com www.manning.com/books/swift-in-depth

  3. WHAT WE’LL COVER ▸ Swift’s error handling ▸ Result’s purpose

    ▸ Dynamic errors handling + Result ▸ Mixing Result with throwing functions ▸ Functional Combinators ▸ Downsides
  4. EVOLUTION OF SWIFT’S ERROR HANDLING

  5. SWIFT LOOKS AT FUNCTIONAL PROGRAMMING LANGUAGES ▸ Higher-order functions ▸

    Optional (Haskell’s Maybe type) ▸ Algebraic data types (structs, tuples, and enums)
  6. SWIFT 2 INTRODUCES ERROR HANDLING

  7. HASKELL’S EITHER TYPE?

  8. HASKELL’S EITHER TYPE? RUST’S RESULT TYPE?

  9. HASKELL’S EITHER TYPE? RUST’S RESULT TYPE?

  10. JAVA-LIKE + OBJ-C BRIDGING HYBRID

  11. enum ImpendingDoomError: ErrorType { case alienAttack case explosion case volcanicEruption

    }
  12. enum ImpendingDoomError: Error { case alienAttack case explosion case volcanicEruption

    }
  13. defer { findShelter() } do { try pressButton() } catch

    ImpendingDoomError.explosion { // handle specific error } catch { // handle anything else }
  14. let error: NSError = ImpendingDoomError.explosion as NSError print(error) // Error

    Domain=com.impendingdoom Code=500 "(null)" UserInfo={Oh no=We are doomed}
  15. enum ImpendingDoomError: CustomNSError { case alienAttack case explosion case volcanicEruption

    static var errorDomain: String { return "com.impendingdoom" } /// The error code within the given domain. var errorCode: Int { return 500 } /// The user-info dictionary. var errorUserInfo: [String : Any] { return ["Oh no" : "We are doomed"] } }
  16. SOME TRADE-OFFS

  17. func pressButton() throws { try warmupMachine() try startChemicalReaction() }

  18. defer { findShelter() } do { try pressButton() } catch

    ImpendingDoomError.explosion { // handle specific error } catch { // handle anything else }
  19. ASYNC ERROR HANDLING?

  20. let url = URL(string: “https://itunes.apple.com/search?term=harry%20potter")! let task = URLSession.shared.dataTask(with: url)

    { (data, response, error) -> Void in if let data = data, error == nil { store(data: data) } else if let error = error { handle(error: error) } else { // both data and error are nil, what now? fatalError("Shouldn't come here ") } } task.resume()
  21. let url = URL(string: “https://itunes.apple.com/search?term=harry%20potter")! let task = URLSession.shared.dataTask(with: url)

    { (data, response, error) -> Void in if let data = data, error == nil { store(data: data) } else if let error = error { handle(error: error) } else { // both data and error are nil, what now? fatalError("Shouldn't come here ") } } task.resume()
  22. let url = URL(string: “https://itunes.apple.com/search?term=harry%20potter")! let task = URLSession.shared.dataTask(with: url)

    { (data, response, error) -> Void in if let data = data, error == nil { store(data: data) } else if let error = error { handle(error: error) } else { // both data and error are nil, what now? fatalError("Shouldn't come here ") } } task.resume()
  23. let url = URL(string: “https://itunes.apple.com/search?term=harry%20potter")! let task = URLSession.shared.dataTask(with: url)

    { (data, response, error) -> Void in if let data = data, error == nil { store(data: data) } else if let error = error { handle(error: error) } else { // both data and error are nil, what now? fatalError("Shouldn't come here ") } } task.resume()
  24. SWIFT LANGUAGE DESIGNERS: “WAIT UNTIL ASYNC/AWAIT”

  25. None
  26. enum Result<Value, Failure: Error> { case success(Value) case failure(Failure) }

    enum Result<Value, Failure> { case success(Value) case failure(Failure) } enum Result<Value> { case success(Value) case error(NSError) case canceled } enum Result<Value> { case success(Value) case failure(Error) }
  27. None
  28. RAW STRINGS DELIMITER SE-0200

  29. #"We can easily use \"quotes"."# We can easily use \"quotes".

  30. CODE: EXAMPLE

  31. OPAQUE RESULT TYPES SE-0244

  32. CODE: EXAMPLE

  33. RESULT IS INTRODUCED SE-0235 AVAILABLE IN SWIFT 5

  34. WHAT IS RESULT?

  35. public enum Result<Success, Failure> where Failure : Error { ///

    A success, storing a `Success` value. case success(Success) /// A failure, storing a `Failure` value. case failure(Failure) // ... rest omitted } public enum Optional<Wrapped> { case none case some(Wrapped) }
  36. WHY IS RESULT?

  37. PREVENTING IMPOSSIBLE PATHS BENEFIT #1

  38. let url = URL(string: “https://itunes.apple.com/search?term=harry%20potter")! let task = URLSession.shared.dataTask(with: url)

    { (data, response, error) -> Void in if let data = data, error == nil { store(data: data) } else if let error = error { handle(error: error) } else { // both data and error are nil, what now? fatalError("Shouldn't come here ") } } task.resume()
  39. let url = URL(string: “https://itunes.apple.com/search?term=harry%20potter")! let task = URLSession.shared.dataTask(with: url)

    { (result: Result<Data, Error>) in switch result { case .success(let data): store(data: data) case .failure(let error): handle(error: error) } } task.resume()
  40. extension URLSession { func dataTask(with url: URL, completionHandler: @escaping (Result<Data,

    Error>) -> Void) -> URLSessionDataTask { return dataTask(with: url) { (data, response, error) in switch (data, error) { case let (data?, nil): completionHandler(Result.success(data)) case let (nil, error?): completionHandler(Result.failure(error)) default: fatalError("Shouldn't come here ") } } } }
  41. COMPILE-TIME KNOWLEDGE BENEFIT #2

  42. let pancake: Pancake = try bakePancake(temperature: 110) /// - Throws:

    BakingError func bakePancake(temperature: Int) throws -> Pancake { if temperature < 100 { throw BakingError.uncookedPancake } else if temperature > 300 { throw BakingError.burntPancake } return Pancake() }
  43. let pancake: Result<Pancake, BakingError> = bakePancake(temperature: 110) func bakePancake(temperature: Int)

    -> Result<Pancake, BakingError> { if temperature < 100 { return Result.failure(BakingError.rawPancake) } else if temperature > 300 { return Result.failure(BakingError.burntPancake) } let pancake = Pancake() return Result.success(pancake) }
  44. let pancake: Result<Pancake, Error> = bakePancake(temperature: 110) func bakePancake(temperature: Int)

    -> Result<Pancake, Error> { if temperature < 100 { return Result.failure(BakingError.rawPancake) } else if temperature > 300 { return Result.failure(BakingError.burntPancake) } else if temperature > 500 { return Result.failure(CookingError.kitchenFire) } let pancake = Pancake() return Result.success(pancake) }
  45. typealias GenericResult<T> = Result<T, Error> func bakePancake(temperature: Int) -> GenericResult<Pancake>

    { if temperature < 100 { return Result.failure(BakingError.rawPancake) } else if temperature > 300 { return Result.failure(BakingError.burntPancake) } else if temperature > 500 { return Result.failure(CookingError.kitchenFire) } let pancake = Pancake() return Result.success(pancake) }
  46. HAPPY PATH PROGRAMMING BENEFIT #3

  47. FUNCTIONAL COMBINATORS

  48. func loadWeatherReport() -> Result<WeatherReport, WeatherError> { // … }

  49. func loadFile(resource: String, type: String) -> Result<Data, Error> { if

    let fileURL = Bundle.main.url(forResource: resource, withExtension: type) { } else { return Result.failure(FileError.couldNotFindFile) } }
  50. func loadFile(resource: String, type: String) -> Result<Data, Error> { if

    let fileURL = Bundle.main.url(forResource: resource, withExtension: type) { } else { return Result.failure(FileError.couldNotFindFile) } }
  51. func loadFile(resource: String, type: String) -> Result<Data, Error> { if

    let fileURL = Bundle.main.url(forResource: resource, withExtension: type) { } else { return Result.failure(FileError.couldNotFindFile) } }
  52. func loadFile(resource: String, type: String) -> Result<Data, Error> { if

    let fileURL = Bundle.main.url(forResource: resource, withExtension: type) { } else { return Result.failure(FileError.couldNotFindFile) } } do { let data = try Data(contentsOf: fileURL) return Result.success(data) } catch { return Result.failure(error) }
  53. func loadFile(resource: String, type: String) -> Result<Data, Error> { if

    let fileURL = Bundle.main.url(forResource: resource, withExtension: type) { } else { return Result.failure(FileError.couldNotFindFile) } } do { let data = try Data(contentsOf: fileURL) return Result.success(data) } catch { return Result.failure(error) }
  54. func loadFile(resource: String, type: String) -> Result<Data, Error> { if

    let fileURL = Bundle.main.url(forResource: resource, withExtension: type) { } else { return Result.failure(FileError.couldNotFindFile) } } do { let data = try Data(contentsOf: fileURL) return Result.success(data) } catch { return Result.failure(error) }
  55. func loadFile(resource: String, type: String) -> Result<Data, Error> { if

    let fileURL = Bundle.main.url(forResource: resource, withExtension: type) { } else { return Result.failure(FileError.couldNotFindFile) } } do { let data = try Data(contentsOf: fileURL) return Result.success(data) } catch { return Result.failure(error) }
  56. func loadFile(resource: String, type: String) -> Result<Data, Error> { if

    let fileURL = Bundle.main.url(forResource: resource, withExtension: type) { } else { return Result.failure(FileError.couldNotFindFile) } } return Result(catching: { try Data(contentsOf: fileURL) })
  57. func loadFile(resource: String, type: String) -> Result<Data, Error> { if

    let fileURL = Bundle.main.url(forResource: resource, withExtension: type) { } else { return Result.failure(FileError.couldNotFindFile) } } return Result(catching: { try Data(contentsOf: fileURL) })
  58. func loadWeatherReport() -> Result<WeatherReport, Error> { let result: Result<Data, Error>

    = loadFile(resource: "weather", type: “”) switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } }
  59. func loadWeatherReport() -> Result<WeatherReport, Error> { let result: Result<Data, Error>

    = loadFile(resource: "weather", type: “json”) switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } }
  60. func loadWeatherReport() -> Result<WeatherReport, Error> { let result: Result<Data, Error>

    = loadFile(resource: "weather", type: “json”) switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } }
  61. func loadWeatherReport() -> Result<WeatherReport, Error> { let result: Result<Data, Error>

    = loadFile(resource: "weather", type: “json”) switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } }
  62. func loadWeatherReport() -> Result<WeatherReport, Error> { let result: Result<Data, Error>

    = loadFile(resource: "weather", type: “json”) switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } }
  63. func loadWeatherReport() -> Result<WeatherReport, Error> { let result: Result<Data, Error>

    = loadFile(resource: "weather", type: “json”) switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } }
  64. Success Failure Data WeatherReport

  65. Result<WeatherReport, Error> Result<Data, Error>

  66. MAP

  67. func loadWeatherReport() -> Result<WeatherReport, Error> { let result: Result<Data, Error>

    = loadFile(resource: "weather", type: “json”) switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } }
  68. func loadWeatherReport() -> Result<WeatherReport, Error> { let result = loadFile(resource:

    "weather", type: "") switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } } let result: Result<WeatherReport, Error> = loadFile(resource: "weather", type: "json") .map({ (data: Data) -> WeatherReport in WeatherReport(data: data) }) return result
  69. func loadWeatherReport() -> Result<WeatherReport, Error> { let result = loadFile(resource:

    "weather", type: "") switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } } let result: Result<WeatherReport, Error> = loadFile(resource: "weather", type: "json") .map({ (data: Data) -> WeatherReport in WeatherReport(data: data) }) return result
  70. func loadWeatherReport() -> Result<WeatherReport, Error> { let result = loadFile(resource:

    "weather", type: "") switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } } let result: Result<WeatherReport, Error> = loadFile(resource: "weather", type: "json") .map({ (data: Data) -> WeatherReport in WeatherReport(data: data) }) return result
  71. func loadWeatherReport() -> Result<WeatherReport, Error> { let result = loadFile(resource:

    "weather", type: "") switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } } let result: Result<WeatherReport, Error> = loadFile(resource: "weather", type: "json") .map({ (data: Data) -> WeatherReport in WeatherReport(data: data) }) return result
  72. func loadWeatherReport() -> Result<WeatherReport, Error> { let result = loadFile(resource:

    "weather", type: "") switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } } let result: Result<WeatherReport, Error> = loadFile(resource: "weather", type: "json") .map({ (data: Data) -> WeatherReport in WeatherReport(data: data) }) return result
  73. func loadWeatherReport() -> Result<WeatherReport, Error> { let result = loadFile(resource:

    "weather", type: "") switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } } let result: Result<WeatherReport, Error> = loadFile(resource: "weather", type: "json") .map({ (data: Data) -> WeatherReport in WeatherReport(data: data) }) return result
  74. func loadWeatherReport() -> Result<WeatherReport, Error> { let result = loadFile(resource:

    "weather", type: "") switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } } let result: Result<WeatherReport, Error> = loadFile(resource: "weather", type: "json") .map({ (data: Data) -> WeatherReport in WeatherReport(data: data) }) return result
  75. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(WeatherReport.init) return result
  76. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) .map(WeatherReport.init) return result
  77. func loadWeatherReport() -> Result<WeatherReport, Error> { let result = loadFile(resource:

    "weather", type: "") switch result { case .success(let data): let report = WeatherReport(data: data) return Result.success(report) case .failure(let error): return Result.failure(error) } } let result: Result<WeatherReport, Error> = loadFile(resource: "weather", type: "json") .map(cleanData) .map(WeatherReport.init) return result
  78. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) .map(WeatherReport.init) return result
  79. Success Failure Data WeatherReport WeatherError

  80. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) .map(WeatherReport.init) return result
  81. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .map { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  82. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .map { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  83. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .map { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  84. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .map { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  85. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .map { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  86. Result<Data, Error> Result<Result<WeatherReport, Error>, Error>

  87. Result<WeatherReport, Error> Result<Data, Error>

  88. FLATMAP

  89. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .map { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  90. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  91. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  92. CONCRETE ERRORS

  93. func loadWeatherReport() -> Result<WeatherReport, WeatherError> { // … } func

    loadWeatherReport() -> Result<WeatherReport, Error> { // … }
  94. Result<WeatherReport, Error> Result<Data, Error> map flatMap

  95. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  96. func loadWeatherReport() -> Result<WeatherReport, Error> { } let result: Result<WeatherReport,

    Error> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, WeatherError> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } }
  97. None
  98. Success Failure Error WeatherError

  99. Result<WeatherReport, WeatherError> Result<WeatherReport, Error>

  100. MAPERROR

  101. func loadWeatherReport() -> Result<WeatherReport, WeatherError> { } let result: Result<WeatherReport,

    WeatherError> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } } .mapError { _error in WeatherError.failedToLoad }
  102. func loadWeatherReport() -> Result<WeatherReport, WeatherError> { } let result: Result<WeatherReport,

    WeatherError> = loadFile(resource: "weather", type: "json") .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } } .mapError { _error in WeatherError.failedToLoad }
  103. ERROR RECOVERY

  104. Success Failure Error (Transformed) Error Recovered Value

  105. FLATMAPERROR

  106. func loadWeatherReport() -> Result<WeatherReport, WeatherError> { } let result: Result<WeatherReport,

    WeatherError> = loadFile(resource: "weather", type: “json”) .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } } .mapError { _error in WeatherError.failedToLoad } .flatMapError { _error -> Result<Data, Error> in loadFile(resource: "weather_backup", type: "json") }
  107. func loadWeatherReport() -> Result<WeatherReport, WeatherError> { } let result: Result<WeatherReport,

    WeatherError> = loadFile(resource: "weather", type: “json”) .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } } .mapError { _error in WeatherError.failedToLoad } .flatMapError { _error -> Result<Data, Error> in loadFile(resource: "weather_backup", type: "json") }
  108. func loadWeatherReport() -> Result<WeatherReport, WeatherError> { } let result: Result<WeatherReport,

    WeatherError> = loadFile(resource: "weather", type: “json”) .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } } .mapError { _error in WeatherError.failedToLoad } .flatMapError { _error -> Result<Data, Error> in loadFile(resource: "weather_backup", type: "json") }
  109. Result<WeatherReport, WeatherError> Result<Data, Error> mapError flatMapError map flatMap

  110. func loadWeatherReport() -> Result<WeatherReport, WeatherError> { } let result: Result<WeatherReport,

    WeatherError> = loadFile(resource: "weather", type: “json”) .map(cleanData) return result .flatMap { (data: Data) -> Result<WeatherReport, Error> in if let report = WeatherReport(data: data) { return Result.success(report) } else { return Result.failure(WeatherReport.failedToLoad) } } .mapError { _error in WeatherError.failedToLoad } .flatMapError { _error -> Result<Data, Error> in loadFile(resource: "weather_backup", type: "json") }
  111. let report: Result<WeatherReport, WeatherError> = loadWeatherReport()

  112. DOWNSIDES OF RESULT

  113. NO ASYNC CHAINING SUPPORT

  114. DEBUGGING PIPELINES IS FINICKY

  115. NO OBJECTIVE-C BRIDGING ☹

  116. NO SYNTACTIC SUGAR

  117. MORE STYLE DISCUSSIONS (TWO IDIOMS)

  118. @tjeerdintveen
 www.swiftindepth.com THANK YOU