Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1801 lines (1299 sloc) 39.1 KB

theme: Plain Jane autoscale: true

Three Stories about Error Handling in Swift

Yuta Koshizawa @koher

^ (0:00, 0:27) I am honored to be with you today at one of the finest Swift conferences in the world. Truth be told, I've never attended a Swift conference and this is my first presentation about Swift. Today I want to tell you three stories about error handling. That's it. No big deal. Just three stories.


The First Story

Meeting the Optionals

^ (0:27, 0:15) The first story is about meeting the Optionals.

^ I think optionals are one of the best features in Swift. So why did I get it?

^ It started before Swift was born.


Error Handling in C

^ (0:42, 0:17) My first actual programming language was C although I fiddled around with BASIC in my childhood.

^ Error handling in C was something like this.

// [ C ]
int *numbers = (int *)malloc(sizeof(int) * 42);
if (numbers == NULL) {
    // Error handling here
}

^ It can be easily forgotten.


Error Handling in C

^ (0:59, 0:09) The C compiler never warns or fails even if we forget to handle errors.

// [ C ]
int *numbers = (int *)malloc(sizeof(int) * 42);
// Easily forgets to handle errors

^ It's unsafe.


Error Handling in Java

^ (1:08, 0:25) After that, I learned checked exceptions in Java, which forced programmers to handle errors.

^ For example, think about a function to parse an integer from a string, which throws a FormatException when the string isn't parsed correctly.

// [ Java ]
static int toInt(String string)
  throws FormatException {
    ...
}
// [ Java ]
toInt("42");    // Success
toInt("Swift"); // Failure

Error Handling in Java

^ (1:33, 0:06) It causes a compilation error without error handling.

// [ Java ]
String string = ...;
int number = toInt(string); // Compilation error

Error Handling in Java

^ (1:39, 0:13) We need to try and catch it.

// [ Java ]
String string = ...;
try {
  int number = toInt(string);
  ...
} catch (FormatException e) {
  // Error handling
  ...
}

^ But sometimes we want to ignore errors. Text fields might force users to input only numbers. Even then, =>


Error Handling in Java

^ (1:52, 0:19) we must write meaningless error handling to ignore the errors.

// [ Java ]
String string = ...;
try {
  int number = toInt(string);
  ...
} catch (FormatException e) {
  // Error handling
  throw new Error("Never reaches here.");
}

^ I discussed this problem with my colleagues at Qoncept, our company, and concluded: we needed an explicit but easy way to ignore errors.


Error Handling in Java

^ (2:11, 0:13) ! after a method call was a good candidate. It's done by typing one key, still explicit and looks dangerous.

// [ Java ]
String string = ...;
int number = toInt(string)!; // Ignores exceptions
  // This `!` was what we wanted.

Optionals for Error Handling

^ (2:24, 0:20) Some years later, I did meet optionals in Swift.

^ Swift provided optionals mainly to eliminate NullPointerExceptions. But they were also used for error handling.

^ toInt is written this way using optionals.

// [ Swift ]
func toInt(string: String) -> Int? {
  ...
}

Optionals for Error Handling

^ (2:44, 0:06) It causes a compilation error without error handling.

// [ Swift ]
let string: String = ...
let number: Int = toInt(string) // Compilation error

Optionals for Error Handling

^ (2:50, 0:06) We can handle errors using optional binding.

// [ Swift ]
let string: String = ...
if let number = toInt(string) {
  ...
} else {
  // Error handling
  ...
}

Optionals for Error Handling

^ (2:56, 0:12) How about ignoring errors?

^ When I learned forced unwrapping, I was so surprised because it was what we exactly wanted.

// [ Swift ]
let string: String = ...
let number: Int = toInt(string)! // Ignores an error

Optionals for Error Handling

^ (3:08, 0:16) Unlike exceptions, optionals don't work well for functions with side effects, often without a return value. But I think the @warn_unused_result attribute can be the solution.

// [ Swift ]
@warn_unused_result
func updateBar(bar: Bar) -> ()? {
  ...
}
// [ Swift ]
foo.updateBar(bar) // Warning

Optionals for Error Handling

^ (3:24, 0:13) Handling and ignoring errors can be done this way.

// [ Swift ]
if let _ = foo.updateBar(bar) {
  ...
} else {
  // Error handling
  ...
}
// [ Swift ]
_ = foo.updateBar(bar) // Ignores the error

^ If we had a kind of the @error_unused_result attribute, it would be better.


Optionals for Error Handling

^ (3:37, 0:23) And, optionals provide more flexible ways to handle errors. While exceptions must be handled just after they are thrown, optionals can be handled lazily.

^ We can assign an optional into a variable, pass it to a function, and store it in a property.

// [ Swift ]
let string: String = ...
let number: Int? = toInt(string)

...

// Errors can be handled lazily
if let number = number {
  ...
} else {
  // Error handling
  ...
}

Difficulty of Using Optionals

^ (4:00, 0:15) It wasn't all romantic. My codes soon got full of optionals.

^ With optionals, it isn't easy to even square a number

// [ Swift ]
let a: Int? = ...
let square = a * a // Compilation error

^ or calculate a sum.

// [ Swift ]
let a: Int? = ...
let b: Int? = ...
let sum = a + b // Compilation error

Difficulty of Using Optionals

^ (4:15, 0:06) Five lines for each by optional binding. It's awful.

// [ Swift ]
let a: Int? = ...
let square: Int?
if let a = a {
  square = a * a
} else {
  square = nil
}
// [ Swift ]
let a: Int? = ...
let b: Int? = ...
let sum: Int?
if let a = a, b = b {
  sum = a + b
} else {
  sum = nil
}

Functional Operations for Optionals

^ (4:21, 0:14) Fortunately, Swift provides functional ways for such cases.

^ map is useful for square,

// [ Swift ]
let a: Int? = ...
let square: Int? = a.map { $0 * $0 }

^ and flatMap for sum to flatten the nested Optionals.

// [ Swift ]
let a: Int? = ...
let b: Int? = ...
let sum: Int? = a.flatMap { a in b.map { b in a + b } }

Functional Operations for Optionals

^ (4:35, 0:16) More optional values make it complicated. A typical case is decoding models from a JSON.

^ Assume we have the APIs like SwiftyJSON's ([^1]).

// [ Swift ]
let id: String? = json["id"].string

^ Any steps of decoding can fail.

// [ JSON ]
// The `json` might not be an `Object`
[ "abc" ]
// It might not have a key named `"id"`
{ "foo": "abc" }
// The value might not be a `String`
{ "id": 42 }

Functional Operations for Optionals

^ (4:51, 0:09) So, all return values are optionals. How can we initialize this Person with these optional values?

// [ Swift ]
struct Person {
  let id: String
  let firstName: String
  let lastName: String
  let age: Int
  let isAdmin: Bool
}

let id: String? = json["id"].string
let firstName: String? = json["firstName"].string
let lastName: String? = json["lastName"].string
let age: Int? = json["age"].int
let isAdmin: Bool? = json["isAdmin"].bool

Functional Operations for Optionals

^ (5:00, 0:06) With flatMap, we get this awful pyramid.

// [ Swift ]
let person: Person? = id.flatMap { id in
  firstName.flatMap { firstName in
    lastName.flatMap { lastName in
      age.flatMap { age in
        isAdmin.flatMap { isAdmin in
          Person(id: id, firstName: firstName,
            lastName: lastName, age: age, isAdmin: isAdmin)
        }
      }
    }
  }
}

Functional Operations for Optionals

^ (5:06, 0:09) Optional binding seems better.

// [ Swift ]
let person: Person?
if let id = id, firstName = firstName,
  lastName = lastName, age = age, isAdmin = isAdmin {
  person = Person(id: id, firstName: firstName,
    lastName: lastName, age: age, isAdmin: isAdmin)
} else {
  person = nil
}

^ But we have to repeat the parameter names so many times.


Functional Operations for Optionals

^ (5:15, 0:15) In an applicative style, which is common in Haskell, it becomes much simpler.

// [ Swift ]
let person: Person? = curry(Person.init) <^> id
  <*> firstName <*> lastName <*> age <*> isAdmin

^ Applicative styles are available in Swift with the third-party library "thoughtbot/Runes" ([^2]).


Syntactic Sugars and Operators for Optionals

^ (5:30, 0:10) Additionally, Swift provided these syntactic sugars and operators to make it easy to use optionals.

// [ Swift ]
//let foo: Optional<Foo> = ...
let foo: Foo? = ...

// let baz: Baz? = foo.flatMap { $0.bar }.flatMap { $0.baz }
let baz: Baz? = foo?.bar?.baz

// let quxOrNil: Qux? = ...
// let qux: Qux
// if let q = quxOrNil {
//   qux = q
// } else {
//   qux = Qux()
// }
let quxOrNil: Qux? = ...
let qux: Qux = quxOrNil ?? Qux()

Optionals in Swift

  • Foo? == Optional<Foo>
  • Forced Unwrapping: !
  • map, flatMap
  • Applicative styles: <^>, <*> ([^2])
  • Optional chaining: foo?.bar?.baz
  • Nil coalescing operator: ??

^ (5:40, 0:26) ? notations, forced unwrapping, map, flatMap, optional chaining, ...

^ Some languages had some of them. But combination of all made Swift different. It was safe, practical, theoretically subtle in a way that other languages couldn't achieve, and I found it fascinating.


^ (6:06, 0:40) Tony Hoare, the inventor of null references, said this.

I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. -- Tony Hoare

^ I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement.

^ I think it is the dark side of programming. Falling to the dark side is easy. In exchange for a little unsafety, we can get free from the complication of types. But I want to stay as a Jedi. I believe it makes the evolution.

^ Optionals were the evolution. They are type safe and still practical. That's the reason why I think optionals in Swift are great.


The Second Story

Success or Failure

^ (6:46, 0:21) My second story is about success or failure.

^ Although optionals were great, it lacked a way to report the causes of errors.

^ Mainly, it leads two problems.

^ 1. Hard to debug.

^ 2. Can't branch operations by the causes of errors.


Problems of Optionals

^ (7:07, 0:12) For example,

// [ Swift ]
let a: Int? = toInt(aString)
let b: Int? = toInt(bString)
let sum: Int? = a.flatMap { a in b.map { b in a + b } }

guard let sum = sum else {
  // Which `a` or `b` failed to be parsed?
  // What string was the input?
  ...
}

^ even for such a simple operation, we want to know which of a or b failed to be parsed and what was the input.


Problems of Optionals

^ (7:19, 0:21) Another example is JSONs'.

^ If we want to get false when the key "isAdmin" is omitted in the JSON, how can we do it by optionals?

// [ Swift ]
let isAdmin: Bool
if let admin = json["isAdmin"].bool {
  // { "isAdmin": true }
  isAdmin = admin
} else {
  // 1. [ true ]
  // 2. {}
  // 3. { "isAdmin": 42 }
  isAdmin = ...
}

^ It can fail in three ways as shown.

^ We want it to recover from only the second case =>


Problems of Optionals

// [ Swift ]
let isAdmin: Bool
if let admin = json["isAdmin"].bool {
  // { "isAdmin": true }
  isAdmin = admin
} else {
  // 1. [ true ]
  // 2. {}                => false
  // 3. { "isAdmin": 42 }
  isAdmin = ...
}

^ (7:39, 0:03) and fail for the others.


Problems of Optionals

// [ Swift ]
let isAdmin: Bool
if let admin = json["isAdmin"].bool {
  // { "isAdmin": true }
  isAdmin = admin
} else {
  // 1. [ true ]          => error
  // 2. {}                => false
  // 3. { "isAdmin": 42 } => error
  isAdmin = ...
}

^ (7:42, 0:05) nil cannot show the difference.


Alternatives of Optionals

^ (7:40, 0:08) I found three solutions for those problems.

  1. Tuples
  2. Union types
  3. Results

^ They are also discussed on the swift-evolution mailing list.


Tuples

^ (7:48, 0:13) With tuples, toInt can be written this way.

// [ Swift ]
func toInt(string: String) -> (Int?, FormatError?) {
  ...
}

^ It returns a FormatError in addition to the Int value. Libraries in Go sometimes applies this style.


Tuples

^ (8:01, 0:08) But it makes four cases of results.

  • (value, nil ) // Success
  • (nil , error) // Failure
  • (value, error) // ???
  • (nil , nil ) // ???

^ I didn't want the last two.


Union types

^ (8:09, 0:18) Union types are provided in Ceylon, TypeScript and Python with type hints.

^ With unions, Int|String means the type Int or String. So we can return Int or FormatError directly.

// [ Swift ]
func toInt(string: String) -> Int|FormatError {
  ...
}
// [ Swift ]
switch toInt(...) {
  case let value as Int:
    ...
  case let error as FormatError:
    // Error handling
    ...
}

Union types

^ (8:27, 0:15) In addition, it's interesting that optionals in Ceylon and Python are a syntactic sugar of unions.

// [ Ceylon ]
Integer? a = 42;
Integer|Null a = 42;
# [ Python ]
def foo() -> Optional[Foo]: ...
def foo() -> Union[Foo, None]: ...

^ Unions are a straightforward way to extend optionals in those languages.


Union types

^ (8:42, 0:11) But the Optional in Swift was an enumeration.

// [ Swift ]
enum Optional<T> {
  case Some(T)
  case None
}

^ I thought it wasn't Swifty to extend optionals by unions.


Results

^ (8:53, 0:12) Results came from Rust.

^ The Result can be declared this way.

// [ Swift ]
enum Result<T, E> {
  case Success(T)
  case Failure(E)
}

^ It's Swifty and a natural extension of the Optional.

// [ Swift ]
enum Optional<T> {
  case Some(T)
  case None
}

Results

^ (9:05, 0:04) With results, we can get error information,

// [ Swift ]
let a: Result<Int, FormatError> = toInt(aString)
let b: Result<Int, FormatError> = toInt(bString)
let sum: Result<Int, FormatError> = a.flatMap { a in b.map { b in a + b } }

switch sum {
  case let .Success(sum):
    ...
  case let .Failure(error):
    // Get the detailed error information from `error`
    ...
}

Results

^ (9:09, 0:16) and branch operations by the causes of errors.

// [ Swift ]
let isAdmin: Bool
switch json["isAdmin"].bool {
  case let .Success(admin):
    isAdmin = admin
  case .Failure(.MissingKey):
    // {}                => false
    isAdmin = false
  case .Failure(.TypeMismatch, .NotObject):
    // [ true ]          => error
    // { "isAdmin": 42 } => error
    ...
}

^ Results can be also mapped and flatMapped like optionals.

^ The library "antitypical/Result" ([^3]) provides such results for Swift.


Results

^ (9:25, 0:25) It would be convenient if results had syntactic sugars like optionals.

^ Although I excluded unions, their vertical bar notations seem intuitive and easy to write. Also flatMap chains should be written like optional chaining.

// [ Swift ]
let foo: Result<Foo, Error> = ...
let baz: Result<Foo, Error>
  = foo.flatMap { $0.bar }.flatMap { $0.baz }
// [ Swift ]
let foo: Foo|Error = ...
let baz: Baz|Error = foo?.bar?.baz

^ They would make results more powerful.


Difficulty of Using Results

^ (9:50, 0:16) Results seemed good. But soon I found some cases they didn't work.

^ This is the example.

// [ Swift ]
let a: Result<Int, ErrorA> = ...
let b: Result<Int, ErrorB> = ...
let sum: Result<Int, ???>
  = a.flatMap { a in b.map { b in a + b } }

^ What should the second type parameter be? It can be both ErrorA and ErrorB.


Difficulty of Using Results

^ (10:06, 0:10) One easy answer was using an union of ErrorA|ErrorB. But that was the one I excluded.

// [ Swift ]
let a: Result<Int, ErrorA> = ...
let b: Result<Int, ErrorB> = ...
let sum: Result<Int, ErrorA|ErrorB>
  = a.flatMap { a in b.map { b in a + b } }

Difficulty of Using Results

^ (10:16, 0:06) The next idea was nested results.

// [ Swift ]
let a: Result<Int, ErrorA> = ...
let b: Result<Int, ErrorB> = ...
let sum: Result<Int, Result<ErrorA, ErrorB>>
  = a.flatMap { a in b.map { b in a + b } }

^ It looked awful.


Difficulty of Using Results

^ (10:22, 0:06) It got better with the vertical bar notations.

// [ Swift ]
let a: Int|ErrorA = ...
let b: Int|ErrorB = ...
let sum: Int|ErrorA|ErrorB
  = a.flatMap { a in b.map { b in a + b } }

Difficulty of Using Results

^ (10:28, 0:10) But it was still bad when I had more results. They were nested too deeply and unintuitively.

// [ Swift ]
let id: String|ErrorA = ...
let firstName: String|ErrorB = ...
let lastName: String|ErrorC = ...
let age: Int|ErrorD = ...
let isAdmin: Bool| ErrorE = ...

let person: Person|(((ErrorA|ErrorB)|ErrorC)|ErrorD)|ErrorE
  = curry(Person.init) <^> id <*> firstName
    <*> lastName <*> age <*> isAdmin

Difficulty of Using Results

^ (10:38, 0:11) Error handling was done this way.

// [ Swift ]
switch person {
  case let .Success(person):
    ...
  case let .Failure(.Success(.Success(.Success(.Success(.Failure(errorA)))))):
    ...
  case let .Failure(.Success(.Success(.Success(.Failure(errorB))))):
    ...
  case let .Failure(.Success(.Success(.Failure(errorC)))):
    ...
  case let .Failure(.Success(.Failure(errorD))):
    ...
  case let .Failure(.Failure(errorD)):
    ...
}

^ It was too complicated.

^ I thought about this problem for a long time. And finally, =>


Results without an Error Type

^ (10:49, 0:08) I concluded that the second type parameter of results was not important in practice.

// [ Swift ]
enum Result<T, E> {
  case Success(T)
  case Failure(E)
}

Results without an Error Type

^ (10:57, 0:21) If the Result is declared this way, it loses the type of the error and seems unsafe.

// [ Swift ]
enum Result<T> {
  case Success(T)
  case Failure(ErrorType)
}

^ But, in most cases, we don't need to branch operations for all possible errors. We just need to care about one or two exceptional ones.


Results without an Error Type

^ (11:18, 0:25) Think about networking operations. They fail in various ways. We want to retry the operation when it gets timeout. But not for "Forbidden", "Not found" and so on.

^ Then we branch the operation into the cases of Success, Timeout and the others. We don't need to list up all possible errors.

// [ Swift ]
downloadJson(url) { json: Result<Json> in
  switch json {
    case let .Success(json): // success
      ...
    case let .Failure(.Timeout): // timeout
      // retry
      ...
    case let .Failure(error): // others
      // error
      ...
  }
}

Results without an Error Type

^ (11:43, 0:17) Also, it's true for JSONs' example. We want to recover only from MissingKey, and raise an error for the others.

// [ Swift ]
let isAdmin: Bool
switch json["isAdmin"].bool {
  case let .Success(admin): // success
    isAdmin = admin
  case .Failure(.MissingKey): // missing key
    // {}                => false
    isAdmin = false
  case let .Failure(error): // others
    // [ true ]          => error
    // { "isAdmin": 42 } => error
    ...
}

^ It's very rare to branch operations for all possible errors. And if we actually need it, =>


Results without an Error Type

^ (12:00, 0:09) we can do it by enumerations with associated values which Swift has already provided.

// [ Swift ]
enum Foo {
  case Bar(A)
  case Baz
  case Qux(B)
}

func foo() -> Foo { ... }

switch foo() {
  case let Bar(a):
    ...
  case let Baz:
    ...
  case let Qux(b):
    ...
}

Results without an Error Type

^ (12:09, 0:14) I implemented a library named "ResultK" ([^4]) to provide such results. It works well even if various types of errors are mixed together.

// [ Swift ]
let a: Result<Int> = ... // ErrorA
let b: Result<Int> = ... // ErrorB
let sum: Result<Int> // ErrorA or ErrorB
  = a.flatMap { a in b.map { b in a + b } }

Results without an Error Type

^ (12:23, 0:23) How about syntactic sugars for them? Int| as Result<Int> might be good.

// [ Swift ]
let a: Int| = ...
let b: Int| = ...
let sum: Int|
  = a.flatMap { a in b.map { b in a + b } }

^ I'm afraid that such results are the dark side. It makes results untyped. But as far as I considered, it is the best way so far.


The Third Story

try

^ (12:46, 0:11) My third story is about try.

^ Swift 2.0 introduced the syntax similar to try / catch in Java.


Automatic Propagation

^ (12:57, 0:23) My first impression was bad. I didn't want to go back to the Java age. But as I learned it, I started to think it was pretty good.

^ The Swift core team explained why they employed the try / catch syntax in the document named "Error Handling Rationale and Proposal" ([^5]).

// [ Swift ]
func toInt(string: String) throws -> Int {
  ...
}

do {
  let number = try toInt(string)
  ...
} catch let error {
  // Error handling here
  ...
}

Automatic Propagation

^ (13:20, 0:23) In the rationale, the core team defined manual propagation and automatic propagation of errors. With manual propagation, errors are handled by a control flow statement manually while, with automatic propagation, it jumps to the handler automatically when an error occurs.

// [ Swift ]
// Manual propagation
switch(toInt(string)) {
  case let .Success(number):
    ...
  case let .Failure(error): // Handles an error manually
    ...
}

// Automatic propagation
do {
  let number = try toInt(string) // Jumps to `catch` automatically
  ...
} catch let error {
  ...
}

Automatic Propagation

^ (13:43, 0:29) Automatic propagation is useful especially when we want to handle multiple errors all together. Even with manual propagation, we can do it in a functional way using map, flatMap and applicative. But it's syntactically complicated and theoretically difficult. It's unreasonable to expect all programmers to understand them.

// [ Swift ]
// Manual propagation
let a: Result<Int> = toInt(aString)
let b: Result<Int> = toInt(bString)
switch a.flatMap { a in b.map { b in a + b } } {
  case let .Success(sum):
    ...
  case let .Failure(error):
    ...
}

// Automatic propagation
do {
  let a: Int = try toInt(aString)
  let b: Int = try toInt(bString)
  let sum: Int = a + b
  ...
} catch let error {
  ...
}

Automatic Propagation

^ (14:12, 0:37) In the rationale, the core team referred to an interesting topic about Haskell's do notation.

^ It's a notation to simplify flatMap chains and nested flatMaps.

// [ Swift ]
let sum = toInt(aString).flatMap { a in
   toInt(bString).flatMap { b in
     .Some(a + b)
   }
}
-- [ Haskell ]
sum = do
  a <- toInt aString
  b <- toInt bString
  Just (a + b)

^ The core team said it was a kind of automatic propagation. It means we anyway need automatic propagation for both functional and non-functional error handling to write it in a simple notation.

^ So I understood it was good to introduce automatic propagation to Swift.


Marked Propagation

^ (14:49, 0:20) I also worried about untyped throws.

^ We can't specify types of errors with a throws clause so far. Although it seems unsafe, I think it's reasonable for the same reason as for result types. What I was worried about was another thing.

// [ Swift ]
func toInt(string: String) throws FormatError -> Int { // Compilation error
  ...
}

Marked Propagation

^ (15:09, 0:16) Java has unchecked exceptions. The compiler reports nothing even if we don't handle them. C# and various dynamically typed languages have a similar mechanism too.

// [ Java ]
class FormatException extends RuntimeException {
  ...
}
// [ Java ]
static int toInt(String string) throws FormatException {
    ...
}
// [ Java ]
String string = ...;
int number = toInt(string); // No compilation error

Marked Propagation

^ (15:25, 0:23) In those languages, every line in a code might throw an unexpected error.

// [ Java ]
void foo() { // What can `foo` throw?
  a(); // May throw an unchecked exception
  b(); // May throw an unchecked exception
  c(); // May throw an unchecked exception
  d(); // May throw an unchecked exception
  e(); // May throw an unchecked exception
  f(); // May throw an unchecked exception
  g(); // May throw an unchecked exception
}

^ Then, no one knows what kinds of errors can be actually thrown even by a function she implemented. It's so bad. Impossible to complete error handling, and we tend to be careless about it.


Marked Propagation

^ (15:48, 0:27) I thought it could be reproduced in Swift.

^ Swift doesn't have unchecked exceptions. But once we add throws to a function, it's hard to know which lines in the function can throw an error. And because we don't need to specify the types of the errors, we get careless about what kinds of errors the function throws.

// [ Swift ]
func foo() throws { // What can `foo` throw?
  a() // Can throw an error?
  b() // Can throw an error?
  c() // Can throw an error?
  d() // Can throw an error?
  e() // Can throw an error?
  f() // Can throw an error?
  g() // Can throw an error?
}

Marked Propagation

^ (16:15, 0:38) But Swift forces to add the keyword try when we call a function with throws. The core team called it marked propagation.

// [ Swift ]
func foo() throws {
  a()
  try b() // May throw an error
  c()
  d()
  try e() // May throw an error
  f()
  g()
}

^ With try, it's obvious which lines can throw an error. And it makes it much easier to check what kinds of errors can be thrown in the function.

^ If throws were typed, it would be safer. But I think marked propagation removed the worst part of untyped throws, and untyped throws is a reasonable trade-off between type safety and simplicity.


Marked Propagation

^ (16:53, 0:25) Marked propagation also helps us to read codes. With automatic propagation, it's hard to understand the control flow from where it jumps to catch clauses. It's referred as an implicit control flow problem in the rationale. Marked propagation makes it clearer.

// [ Java ]
try {
  foo();
  bar();
  baz();
} catch (QuxException e) {
  // Where did it come from?
}
// [ Swift ]
do {
  foo()
  try bar()
  baz()
} catch let error {
  // Came from `bar()`
}

Marked Propagation

  • Careless about error types
  • Implicit control flow

^ (17:18, 0:09) So, Marked propagation is a solution for these two problems. I thought it was evolutional.


Marked Automatic Propagation for Optionals

^ (17:27, 0:22) Now we have a question. Marked automatic propagation seems good. Why don't we use it for optionals?

^ In the rationale, the core team says optionals should be used for simple domain errors and manual propagation is suitable for them. toInt was an example they gave.

// [ Swift ]
// Optionals for a simple domain error
func toInt(string: String) -> Int? {
  ...
}

// Manual propagation
guard let number = toInt(string) {
  // Error handling here
  ...
}

Marked Automatic Propagation for Optionals

^ (17:49, 0:39) But I think automatic propagation is also useful for optionals. We get nil not only as errors but also just as empty values. Our codes are full of optionals. Handling them manually costs a lot.

^ I propose automatic propagation for optionals this way.

// [ Swift ]
// Manual propagation
let a: Int? = toInt(aString)
let b: Int? = toInt(bString)
if let sum = (a.flatMap { a in b.map { b in a + b } }) {
    ...
} else {
    ...
}

// Automatic propagation
do {
  let a: Int = try toInt(aString)
  let b: Int = try toInt(bString)
  let sum: Int = a + b
  ...
} catch {
  ...
}

^ In this syntax, try is a kind of unwrapping. We must catch it or return an optional value.

^ I think this syntax is consistent.


Results and try

^ (18:28, 0:13) This can be extended to results.

^ results and throws can be theoretically interchanged. If throws were a syntactic sugar of returning a result value, =>

// [ Swift ]
func toInt(string: String) throws -> Int {
  ...
}
// [ Swift ]
func toInt(string: String) -> Result<Int> {
  ...
}

Results and try

^ (18:41, 0:22) we could connect the both worlds of results and throws seamlessly.

// [ Swift ]
do {
  let a: Int = try toInt(aString)
  let b: Int = try toInt(bString)
  let sum: Int = a + b
  ...
} catch {
  ...
}
// [ Swift ]
let a: Result<Int> = toInt(aString)
let b: Result<Int> = toInt(bString)
switch a.flatMap { a in b.map { b in a + b } } {
  case let .Success(sum):
    ...
  case let .Failure(error):
    ...
}

^ Results provide a more flexible way to handle errors like optionals. They can be handled lazily. So interoperability between them is important.

^ Let me show an example.


Results and try

^ (19:03, 0:20) I implemented the library "ListK" ([^6]) which provides lazily evaluated Lists. It makes it possible to create infinite lists.

^ In spite that they are infinite, they can be mapped because the operations are evaluated lazily.

// [ Swift ]
let infinite: List<Int> = List { $0 } // [0, 1, 2, 3, 4, ...]
let square: List<Int> = infinite.map { $0 * $0 } // [0, 1, 4, 9, 16, ...]

Results and try

^ (19:23, 0:10) But it doesn't work well for a function with throws.

// [ Swift ]
func toInt(string: String) throws -> Int {
  ...
}

let strings: List<String> = ... // ["0", "1", "2", ...]
do {
  // Never finishes
  let numbers: List<Int> = try strings.map(transform: toInt)
} catch let error {
  ...
}

^ This map operation never finishes. Why?


Results and try

^ (19:33, 0:18) map with throws can be written this way by results.

// [ Swift ]
// By throws
func map<U>(transform: T throws -> U) throws -> List<U>


// By `Result`
func map<U>(transform: T -> Result<U>) -> Result<List<U>>

^ Because it must choose Success or Failure to return a result value, it must be evaluated immediately, and never finishes.

^ What I want for my List is =>


Results and try

^ (19:51, 0:07) this. Result and List are swapped. This can be evaluated lazily.

// [ Swift ]
// By throws
func map<U>(transform: T throws -> U) throws -> List<U>
func map<U>(transform: T throws -> U) -> List<Result<U>>

// By `Result`
func map<U>(transform: T -> Result<U>) -> Result<List<U>>
func map<U>(transform: T -> Result<U>) -> List<Result<U>>

Results and try

^ (19:58, 0:11) It enables us to map infinite Lists by a function with throws this way.

// [ Swift ]
func toInt(string: String) throws -> Int {
  ...
}

let a: List<String> = ... // ["0", "1", "2", ...]
let b: List<Result<Int>> = strings.map(transform: toInt)
  // [Result(0), Result(1), Result(2), ...]
let c: List<Result<Int>> = numbers.take(10)
  // [Result(0), Result(1), Result(2), ..., Result(9)]
let d: Result<List<Int>> = sequence(first10)
  // Result([0, 1, 2, ..., 9])
do {
  let e: List<Int> = try d // [0, 1, 2, ..., 9]
  ...
} catch let error {
  // Handling `FormatError`
  ...
}

^ throws as Result made it possible.


Results and try

^ (20:09, 0:29) Let me show you one downside of throws as Result.

^ When we forget to write the keyword try, with Swift 2.x, we get a compilation error just where we omit try.

// Swift 2.x
let a = toInt(aString) // Compilation error here
let b = toInt(bString)
let sum = a + b

^ But with throws as Result, we get a compilation error where we try to use the result value.

// Swift with my proposal
let a = toInt(aString)
let b = toInt(bString)
let sum = a + b // Compilation error here

^ It's confusing and nonintuitive. But totally, I think throws as Result is better.


Asynchronous Operations and try

^ (20:38, 0:37) Moreover, I think try can be used for other purposes besides error handling. One example is for asynchronous operations.

^ JavaScript natively supports the Promise for asynchronous operations. Its then method is theoretically equivalent to map and flatMap. I implemented the Promise library ([^7]) for Swift with map and flatMap.

// [ Swift ]
let a: Promise<Int> = asyncGetInt(...)
let b: Promise<Int> = asyncGetInt(...)
let sum: Promise<Int> = a.flatMap { a in b.map { b in a + b } }

^ It looks exactly like the Result.

// [ Swift ]
let a: Result<Int> = failableGetInt(...)
let b: Result<Int> = failableGetInt(...)
let sum: Result<Int> = a.flatMap { a in b.map { b in a + b } }

^ The only difference is asynchronous or failable.


Asynchronous Operations and try

^ (21:15, 0:28) The future JavaScript is going to support the async / await syntax originated in C#. That syntax is backed by the Promise and makes it easier to write then chains.

^ I think we'll need to discuss the async / await syntax in Swift because asynchronous operations are one of the hottest topics in programming today.

// [ C# ]
async Task<int> AsyncGetInt(...) {
  ...
}

async void PrintSum() {
  int a = await AsyncGetInt(...);
  int b = await AsyncGetInt(...);
  Console.WriteLine(a + b);
}
// [ Swift ]
func asyncGetInt(...) async -> Promise<Int> {
  ...
}

func printSum() async {
  let a: Int = await asyncGetInt(...)
  let b: Int = await asyncGetInt(...)
  print(a + b)
}

Asynchronous Operations and try

^ (21:43, 0:24) The async / await syntax in C# is used like the upper one. This Task class in C# is equivalent to the Promise.

// [ C# ]
async Task<int> AsyncGetInt(...) {
  ...
}

async void PrintSum() {
  int a = await AsyncGetInt(...);
  int b = await AsyncGetInt(...);
  Console.WriteLine(a + b);
}

^ If we had this syntax in Swift, it would be something like the lower one.

// [ Swift ]
func asyncGetInt(...) async -> Promise<Int> {
  ...
}

func printSum() async {
  let a: Int = await asyncGetInt(...)
  let b: Int = await asyncGetInt(...)
  print(a + b)
}

^ I want to change it in Swift to wrapping a return value in a Promise implicitly.


Asynchronous Operations and try

// [ Swift ]
func asyncGetInt(...) async -> Int { // <- Changed Here
  ...
}

func printSum() async {
  let a: Int = await asyncGetInt(...)
  let b: Int = await asyncGetInt(...)
  print(a + b)
}
// [ Swift ]
func asyncGetInt(...) async -> Promise<Int> {
  ...
}

func printSum() async {
  let a: Int = await asyncGetInt(...)
  let b: Int = await asyncGetInt(...)
  print(a + b)
}

^ (22:07, 0:09) Now we can see the common relations between async / await and throws / try.


Asynchronous Operations and try

// [ Swift ]
func asyncGetInt(...) async -> Int {     // async
  ...
}

func printSum() async {                  // async
  let a: Int = await asyncGetInt(...)    // await
  let b: Int = await asyncGetInt(...)    // await
  print(a + b)
}
// [ Swift ]
func failableGetInt(...) throws -> Int { // throws
  ...
}

func printSum() throws {                 // throws
  let a: Int = try failableGetInt(...)   // try
  let b: Int = try failableGetInt(...)   // try
  print(a + b)
}

^ (22:16, 0:24) The async / await syntax is backed by the Promise, and by my proposal, the throws / try syntax is backed by the Result. It perfectly makes sense. async, await, Promise and throws, try, Result represent a common concept only different in a point: asynchronous or failable.


Asynchronous Operations and try

^ (22:40, 0:32) It's possible to unite them by using try as await and just returning Promise values.

// [ Swift ]
func asyncGetInt(...) async -> Int {     // async
  ...
}

do {
  let a: Int = await asyncGetInt(...)    // await
  let b: Int = await asyncGetInt(...)    // await
  print(a + b)
}
// [ Swift ]
func asyncGetInt(...) -> Promise<Int> {  // Promise
  ...
}

do {
  let a: Int = try asyncGetInt(...)      // try
  let b: Int = try asyncGetInt(...)      // try
  print(a + b)
}

^ It's good to have independent keywords like async, await and reasync because it makes it easier to read the codes. But it needs extra new keywords endlessly when we want to add new features.

^ I'm wondering which one is better. I just wanted to show you the possibilities of try.


Let's Discuss Error Handling

^ (23:12, 0:28) I introduced my several ideas.

  • Result<T> instead of Result<T, E>
  • Automatic propagation for Optionals
  • Interoperation between throws and Result
  • try for asynchronous operations

... and let me know your opinions

^ I'm not perfectly sure that they are all good. So please let me know your opinions. I want to discuss error handling in Swift through this conference. And I will join in the swift-evolution mailing list.


^ (23:40, 0:48) I'm dreaming of a world where everyone has been educated in programming. I even tried to design my own programming language suitable for education.

^ One morning, I met Swift. Swift seemed adequate for my purpose. Now I plan to write a free online book for everyone to learn wide programming concepts, from "Hello, world!!" to monads, all in Swift.

^ Through my experience of designing programming languages, I can say that it is a struggle against unsafety and complexity. It can be said in other words: =>


Stay Typed. Stay Practical.

^ (24:28, 0:28) "Stay Typed. Stay Practical."

^ I'm sure this will make the evolution as I talked through my presentation. Stay Typed. Stay Practical. And I have always wished that for Swift's designers. And now, as Swift became open source, I wish that for us.

^ Stay Typed, Stay Practical.

^ Thank you all very much.


All my codes in the slides are available at

koherent.org/tryswift/

I will also post some further topics related to this talk there.

^ [^8]

[^1]: "SwiftyJSON", https://github.com/SwiftyJSON/SwiftyJSON

[^2]: "thoughtbot/Runes", https://github.com/thoughtbot/Runes

[^3]: "antitypical/Result", https://github.com/antitypical/Result

[^4]: "ResultK", https://github.com/koher/ResultK

[^5]: "Error Handling Rationale and Proposal", https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst

[^6]: "ListK" https://github.com/koher/ListK

[^7]: "PromiseK" https://github.com/koher/PromiseK

[^8]: "Steve Jobs' Commencement address (2005)" http://news.stanford.edu/news/2005/june15/jobs-061505.html