[swift-evolution] [Concurrency] async/await + actors

Mike Sanderson m at mikesand.com
Tue Aug 22 11:08:39 CDT 2017

On Mon, Aug 21, 2017 at 4:09 PM, Karim Nassar via swift-evolution <
swift-evolution at swift.org> wrote:

> Thought about it in more depth, and I’m now firmly in the camp of:
> ‘throws’/‘try' and ‘async’/‘await' should be orthogonal features. I think
> the slight call-site reduction in typed characters ('try await’ vs ‘await’)
> is heavily outweighed by the loss of clarity on all the edge cases.

My concern is less for' ‘throws’/‘async' in declarations and ‘try’/‘await'
at call sites ( I can live with both for clarity and explicitness) than it
is for the enclosing 'beginAsync/do'. Johnathan Hull made a similar point
on another thread

The principle at work, that aligns with the concurrency manifesto 'design'
section, is that it should not be the case that handling errors is more
onerous than ignoring or discarding them. Nor should handling errors
produce ugly code.

If handling errors requires nesting a do/try/catch in a beginAsync block
every time, then code that ignored errors will always look cleaner than
responsible code that handles them. Not handling errors will be the
default. If there are specific recoverable errors, like moving a file from
a download task only to find something else already moved it there, now
there are multiple nested do/try/catch.

(I'm not sure what the reasoning is that most users won't have to interact
with primitive  `beginAsync`. That seems like it would be the starting
point for every use of any asynchronous function, especially in iOS
development, as seen in the IBAction example)

Leaving explicit throws/async in declarations and try/await at call sites,
a narrower modification would be:

1.  Make every `beginAsync` coroutine start also a `do` block. The `catch`
block would be necessary only if something throws-- currently a `do` block
can be used this way, so it matches nicely.

(This is the same as Johnathan Hull noted on the thread, that if a goal is
to eliminate the pyramid of doom, requiring two levels of indentation to do
anything isn't a clear win.)

2. The two syspendAsync calls should be only the one:
func suspendAsync<T>(
    _ body: (_ continuation: @escaping (T) -> (),
    _ error: @escaping (Error) -> ()) -> ()
) async throws -> T

We can assume the programmers using this function basically know what
they're doing (not the same as being unwilling to cut corners, so make them
cut corners explicitly and visibly). If a user of this function knows that
no errors are possible, then then it should be called with try! and the
error closure ignored, presumably by replacing it with `_`. To force `try!
suspendAsync` and then call the error closure would be a programmer error.
It would also be a programmer error to keep the throwing function and then
not call the error block, but they can do that with the API as proposed--that's
just harder with the API as proposed because the single-continuation
suspend method makes it easy to write code that ignores errors.

We are talking not only about a language change, but making all Swift
programmers adopt a new methodology. It's an opportunity to build new
habits, as noted in the manifesto, by placing the right thing to do at
hand. Two years ago, moving from passing a pointer to an NSError to
do-try-catch--and truly no one ever used that pattern outside Cocoa
frameworks, a crisis-level problem in error handling-- it was a huge
obvious win. It made dealing with errors so much better. Completion blocks
are not at the level of NSError-pointer-pointer level crisis, but
regardless this should have similar improvement when doing the right thing.

(And my opinion on try?/try!: `try?` I seldom see used, find it be an
anti-pattern of ignoring errors instead of explicitly recovering; I
actually wish it wasn't in the language, but guess some people find
it useful. `try!` is necessary and useful for cases where the compiler
can't guarantee success and the programmer is willing to assert it's not
going to fail, and `!` marks those points in code nicely, matching the
optional syntax.)

About potential await? and await!: If we kept call sites `try await` (or
`await try`?) then the `try?/try!` semantics would behave the same,
another argument for that. I assume `await!` would have the current queue
block until the function returns.

The stronger need is for better recognition that often async functions will
_sometimes_ have their values or errors immediately, if cached, or known to
be impossible to get. In promise/futures, this is the equivalent of
creating a future already fulfilled with a value or error. This might just
be an educational point, though.

Maybe the use of `await?` could be this, checking if the value exists
already-- fulfill the value if it can without suspending, but return nil if
not--throwing if known error, unless try? also so annotated.

Really interesting and insightful comments on this thread, look forward to
seeing how this further evolves.

Mike Sanderson

> —Karim
> On Aug 21, 2017, at 1:56 PM, John McCall <rjmccall at apple.com> wrote:
> On Aug 20, 2017, at 3:56 PM, Yuta Koshizawa <koher at koherent.org> wrote:
> 2017-08-21 2:20 GMT+09:00 John McCall via swift-evolution <swift-evoluti
> on at swift.org>:
>> On Aug 19, 2017, at 7:17 PM, Chris Lattner via swift-evolution <
>> swift-evolution at swift.org> wrote:
>> On Aug 19, 2017, at 8:14 AM, Karim Nassar via swift-evolution <
>> swift-evolution at swift.org> wrote:
>> This looks fantastic. Can’t wait (heh) for async/await to land, and the
>> Actors pattern looks really compelling.
>> One thought that occurred to me reading through the section of the
>> "async/await" proposal on whether async implies throws:
>> If ‘async' implies ‘throws' and therefore ‘await' implies ‘try’, if we
>> want to suppress the catch block with ?/!, does that mean we do it on the
>> ‘await’ ?
>> guard let foo = await? getAFoo() else {  …  }
>> Interesting question, I’d lean towards “no, we don’t want await? and
>> await!”.  My sense is that the try? and try! forms are only occasionally
>> used, and await? implies heavily that the optional behavior has something
>> to do with the async, not with the try.  I think it would be ok to have to
>> write “try? await foo()” in the case that you’d want the thrown error to
>> turn into an optional.  That would be nice and explicit.
>> try? and try! are quite common from what I've seen.
> As analogous to `throws` and `try`, I think we have an option that
> `await!` means blocking.
> First, if we introduce something like `do/catch` for `async/await`, I
> think it should be for blocking. For example:
> ```
> do {
>   return await foo()
> } block
> ```
> It is consistent with `do/try/catch` because it should allow to return a
> value from inside `do` blocks for an analogy of `throws/try`.
> ```
> // `throws/try`
> func foo() -> Int {
>   do {
>     return try bar()
>   } catch {
>     ...
>   }
> }
> // `async/await`
> func foo() -> Int {
>   do {
>     return await bar()
>   } block
> }
> ```
> And `try!` is similar to `do/try/catch`.
> ```
> // `try!`
> let x = try! foo()
> // uses `x` here
> // `do/try/catch`
> do {
>   let x = try foo()
>   // uses `x` here
> } catch {
>   fatalError()
> }
> ```
> If `try!` is a sugar of `do/try/catch`, it also seems natural that
> `await!` is a sugar of `do/await/block`. However, currently all `!` in
> Swift are related to a logic failure. So I think using `!` for blocking is
> not so natural in point of view of symbology.
> Anyway, I think it is valuable to think about what `do` blocks for
> `async/await` mean. It is also interesting that thinking about combinations
> of `catch` and `block` for `async throws` functions: e.g. If only `block`,
> the enclosing function should be `throws`.
> Personally, I think these sources of confusion are a good reason to keep
> the feature separate.
> The idea of using await! to block a thread is interesting but, as you say,
> does not fit with the general meaning of ! for logic errors.  I think it's
> fine to just have an API to block waiting for an async operation, and we
> can choose the name carefully to call out the danger of deadlocks.
> John.
> That aside, I think `try!` is not so occasional and is so important.
> Static typing has limitations. For example, even if we has a text field
> which allows to input only numbers, we still get an input value as a string
> and parsing it may fail on its type though it actually never fails. If we
> did not have easy ways to convert such a simple domain error or a
> recoverable error to a logic failure, people would start ignoring them as
> we has seen in Java by `catch(Exception e) {}`. Now we have `JSONDecoder`
> and we will see much more `try!` for bundled JSON files in apps or
> generated JSONs by code, for which decoding fails as a logic failure.
> --
> Yuta
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170822/89849efd/attachment.html>

More information about the swift-evolution mailing list