[swift-evolution] throws as returning a Result

Yuta Koshizawa koher at koherent.org
Mon Mar 14 06:25:28 CDT 2016


This is some supplementary explanations about throws as returning a Result.

   - What's Result?
   - Why we need both throws and Result
   - Result<Value> vs Result<Value, Error>
   - When forgets try
   - With side effects
   - Why not Either, union types nor tuples
   - Return value of do
   - Complication with rethrows

<https://gist.github.com/koher/e6a8b128bd7ad6898ac9#whats-result>What's
Result?

It's an enum declared in a following way.

enum Result<Value> {
  case Success(Value)
  case Failure(ErrorType)
}

It also should have map, flatMap and some convenient methods like Optional.
<https://gist.github.com/koher/e6a8b128bd7ad6898ac9#why-we-need-both-throws-and-result>Why
we need both throws and Result

Result provides more flexible way to handle errors than throws though they
provide similar functionalities. It can be assigned to a variable, passed
to a function and stored in a property while an error must be handled
immediately after it is thrown. It is useful especially for asynchronous
operations.

For example, think about map or flatMap (or then) method of Promise<Value>
 (or Future<Value>). They cannot receive a function with throws.

extension Promise {
  func map<T>(transform: Value -> T) -> Promise<T> { ... }
}

Because the transform is executed asynchronously, this map method cannot
throw an error immediately. If we had throws as returning a Result, we can
pass a function with throws to the map.

func toInt(x: String) throws -> Int { ... } // -> Result<Int>
let string: Promise<String> = ...let number: Promise<Result<Int>> =
string.map(toInt)

It also caused a problem when I implemented lazily evaluated List<Element>s.

extension List {
  func map<T>(transform: Element -> T) -> List<T> { ... }
}

It cannot throw an error because the transform is evaluated lazily. With
throws as returning a Result, it could be used with a function with throws
 too.

func toInt(x: String) throws -> Int { ... } // -> Result<Int>
let strings: List<String> = ... // Infinite listlet numbers:
List<Result<Int>> = strings.map(toInt)let first10: List<Result<Int>> =
numbers.take(10)let result: Result<List<Int>> = sequence(first10) //
List<Result<...>> -> Result<List<...>>do {
  let mapped: List<Int> = try result
  ...
} catch let error {
  ...
}

If Result is more flexible than throws, why do we need throws? Handling
Results manually with *manual propagation *costs more. We should have a way
to handle errors with *automatic propagation*.

So we need both throws and Result.
<https://gist.github.com/koher/e6a8b128bd7ad6898ac9#resultvalue-vs-resultvalue-error>
Result<Value> vs Result<Value, Error>

I know it is discussed which of *untyped throws* and *typed throws* are
better. If *typed throws* is accepted, Result<Value, Error> should be
provided instead of Result<Value>

However the proposal about *typed throws*
<https://github.com/apple/swift-evolution/pull/68> has been left for
several months. I'm not sure if the discussion is continued. So I started
this thread with Result<Value>. And even if *typed throws* is accepted, we
just need to change Result<Value> to Result<Value, Error>. The discussion
for throws as returning a Result can be applied for *typed throws* and
Result<Value,
Error> as it is.
<https://gist.github.com/koher/e6a8b128bd7ad6898ac9#when-forgets-try>When
forgets try

If we forget to write try, what will happen? This is a downside of throws as
returning a Result.

Even if we forget to write try, it can raise an compilation error with
throws as returning a Result. However the error sites are confusing and
nonintuitive.

func toInt(x: String) throws -> Int { ... } // -> Result<Int>
let a = toInt(aString) // Compilation error here with Swift 2.Xlet b =
toInt(bString)let sum = a + b // Compilation error here with `throws`
as returning a `Result`

I think it can be eased by improved error messages.
<https://gist.github.com/koher/e6a8b128bd7ad6898ac9#with-side-effects>With
side effects

If a function has side effects, its error should not be ignored implicitly.

func update(x: Int) throws { ... } // -> Result<()>

update(42) // No compilation error => dangerous!!

So I think throws should add the @warn_unused_result attribute to the
function automatically. If we had a kind of at error_unused_result attribute,
it would be better.

update(42) // Warning or Error
_ = update(42) // Ignores error explicitly
// Manual propagationswitch update(42) {
  case .Success:
    ...
  case .Failure(error):
    ...
}
// Automatic propagationdo {
  try update(42)
  ...
} catch let error {
  ...
}

<https://gist.github.com/koher/e6a8b128bd7ad6898ac9#why-not-either-union-types-nor-tuples>Why
not Either, union types nor tuples

Result is preferred to Either as discussed on this thread
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160125/007728.html>
.

Either is a tagged union. However its tags are meaningless: Left and Right.
I think it is much better to have something like *union types* in Ceylon
and some other languages than to have Either.

However *union types* make a significant impact on the type system.
Subtyping gets much more complicated. We should also think about *intersection
types* in addition to *uniton types*. If we had *union types*, I think it
would be better that Optional<Foo> is a sugar of Foo|Nil like in Ceylon.
Changing all of them is not practical.

How about tuples? Tuples like (Value?, Error?) is an easy way. However it
results four cases: (value, nil), (nil, error), (value, error) and (nil,
nil). We don't need the last two.

Therefore I think Result is the best way.
<https://gist.github.com/koher/e6a8b128bd7ad6898ac9#return-value-of-do>Return
value of do

We easily think of a return value of do statement/expression like Haskell's
do notation.

func toInt(x: String) throws -> Int { ... } // -> Result<Int>
let sum: Result<Int> = do {
  let a: Int = try toInt("2")
  let b: Int = try toInt("3")
  a + b
} // Result(5)

It can be regarded as a syntactic sugar of nested flatMaps.

However it causes following problems.

   1. It made it impossible to return, break nor continue inside the do
    statement/expression.
   2. Returning the evaluated value of the last expression in braces is not
   Swifty.

I think the following can be the alternative.

let sum: Result<Int> = { () throws -> Int in
  let a: Int = try toInt("2")
  let b: Int = try toInt("3")
  return  a + b
}()

<https://gist.github.com/koher/e6a8b128bd7ad6898ac9#complication-with-rethrows>Complication
with rethrows

With throws as returning a Result, what should be the type of the following
numbers?

func toInt(x: String) throws -> Int { ... } // -> Result<Int>
let numbers: ??? = ["one", "2", "3", "four", "5"].map(toInt)

It can be regarded as both Result<List<Int>> and List<Result<Int>>. I think
it should be decided by the type of the numbers.

// Both workslet numbers: Result<Array<Int>> = ["one", "2", "3",
"four", "5"].map(toInt)let numbers: Array<Result<Int>> = ["one", "2",
"3", "four", "5"].map(toInt)

If the type of the numbers are omitted, what happens? We have some options.

   - Compilation error because of the ambiguous type
   - The default type like that the one for integer literals is Int
      - e.g. If map is marked as rethrows, returns Result<Array<Int>>, and
      Array<Result<Int>> for the others.


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


More information about the swift-evolution mailing list