Await/Async, part deux

I think you are missing the point of async await, it you assume that using await means that the function is no longer async (your frobulate example). Await must not block, but if the function isn’t async, there is no way around blocking.

Your point that in the async await case the type is actually preserved is incorrect, as the input of an async function is a, (b) -> void and the output ist void. I get that this is not what you are trying to say and that I am being pedantic, but what is it that you are really trying to say then?

If you are trying to say that async functions can be chained in an obvious way, then this is true for throwing functions as well. Throwing functions really are arrows from A to B - in a particular Kleisli category. Just like async functions are arrows from A to B in another Kleisli category.

All that can really be said here is that the boilerplate hidden behind some fancy syntax can be branching

switch result{
case success(let value):
...
case failure(let error):
...
}

or nonbranching

completion(result)

But both fundamentally just hide some boilerplate.

2 Likes

I've got a couple of questions about what we are expecting this code to do, apologies if it's been covered already...

What is the purpose of the await keyword here, given that the calling method is synchronous? Isn't the awaiting implicit, given that the function has to run synchronously? If it's just for signalling to the user, that's fine, just wondering if it is necessary.

So in the Swift model, is an asynchronous function returning A actually returning an A, or is it a subtly nested type, like try/catch? Because the value of 'b' in your sync and async methods is a different thing - the former is clearly the 'result', but what is the latter? Does the compiler transparently add an await if you pass the un-awaited result of an async function as a parameter to another function that is expecting the result?

This was a really good explanation.

Is it a fair constraint then that a function which uses await must be async if it has a return value?

2 Likes

I don't think so. Consider:

func a() async -> Int { 3 }

func f() -> String {
    _ = a()

    return "Hi"
}

There's no reason that should not be legal. Besides for the optimizer seeing that a() has no side effects and optimizing it out, all this does is cause a() to be run concurrently with f(), since f isn't waiting for the result.

2 Likes

This strikes me as a potential foot-gun, especially to beginners and those new to async/await. But maybe I’m just overly cautious?

Especially given that the above could easily be refactored to move the async a() func “down” a level and still meet the above rule.

I don't know what you mean. How would you change the example to meet the proposed rule that all functions which return values are marked async while also keeping QuinceyMorris's requirement that functions which invoke async functions need not be marked async?

func a() async -> Int { 3 }

func doA() { _ = await a() }

func f() -> String {
    doA()
    return "Hi"
}

The doA wrapper function makes the continuation of a irrelevant to the flow of f

I don't see the benefit. Invoking a() without await also makes the continuation irrelevant. What are you trying to protect against?

Would you also be satisfied if the compiler warned about invoking an async function with a return value without calling await, unless the return value is explicitly discarded, as in my example?

I think that’s a reasonable compromise.

I feel like there should be some indication that something async is happening, but again, I might simply be being too cautious.

It's a great writeup but I think it would be very helpful to people who never heard of async await if you instead used functions that return something. Then the difference between normal sequential execution and this becomes much clearer.

Also, regarding your A1 example, I think it mostly ads confusion, it doesn't seem like a very useful case, in any case it is not really what async await is meant to achieve, as far as I know. To be honest I'm not even entirely sure what it achieves, it seems it is identical to wait on EventLoopFuture?

I mean, since each call has to complete before moving on, you must be blocking the context in A1, and presumably you want A1 itself to be blocking?

There is a valid reason. Consider a slightly more complex example:

var count = 0
func synchronous() -> C{
  let myCount = count + 1
  let a = synchronous_1()
  let b = synchronous_2(a)
  let c = await asynchronous_3(b) // #1
  count = myCount // #2
  return c
}
func doOther() {
 count -= 1
}

In this code, the value of instance property count can change after asynchronous_3 is entered (#1), but before the line that sets it (#2), if something else invokes doOther on the same thread. (This is a race condition that exists in the current completion-handler-based pattern too: it's not something caused by await.)

It seems reasonable to make the asynchronous function call with await to indicate that this path of execution is temporarily yielding the thread it's running on.

This is an issue as yet unresolved. In the existing proposal, the return value is an A, and there's no way to call the async method without waiting for a A.

In practice, we're almost certainly going to want to have a way of invoking an asynchronous method without waiting, which means some solution like a promise or a future. What the syntax of that might be is unclear.

I'm mostly staying away from this issue for now, because this is a question of how concurrency would work, and there's no point in starting that discussion until we know how the more crucial non-concurrent case works.

2 Likes

Actually, as I said in my previous response, for now it should be regarded as a compilation error ("Function ' a' must be called using the await operator"). I think there should be syntax (other than await) to call 'a' asynchronously, but I don't know what it would be, yet.

The idea is to make it possible to reason about execution flow easily. Your 2-line function 'f' looks like it executes sequentially, so it should execute sequentially, unless there's a great big syntax flag saying it's not sequential.

One possible syntax:

func f() -> String {
    _ = concurrent a()
    return "Hi"
}

Or nowait or future or promise, or even async, or …

Answering no single comment in particular:

Although the current proposal seems to me to mostly take the correct approach, I have a couple of issues with it.

The "important bits" are under the heading "Proposed Solution: Coroutines". In particular, this:

It is important to understand up-front, that the proposed coroutine model does not interface with any particular concurrency primitives on the system: you can think of it as syntactic sugar for completion handlers.

(my boldfacing)

There is currently no restriction that methods with completion handlers can only be called from special contexts. They can be called from any place in existing code.

I don't see anything in the proposal that requires the introduction of a restriction when a call like b() {…} is replaced by the ["you can think of it as"] sugared form await b(). I think the authors of the proposal have simply overlooked the fact, because they're too much swayed by the misanalogy of throws/try.

(Of course, there may be a technical reason. If so, I'll have to think harder about what I'm asking for.)

In fact, I don't see anything in the proposal that requires that a function that uses await to be marked async or yields in its signature. Quite the reverse, it's a yielding method if it contains an await keyword.

What I think async really ought to be for is ["you can think of it as"] sugar for rewriting a method signature:

func b(completion: (Int) -> Void)

in the nicer form:

func b() async -> Int

The proposal is all about the idea of sugaring completion handlers out of visible existence. (See more under the heading "Conversion of imported Objective-C APIs".)

1 Like

After reading all the conversation, I'm still greatly confused by the case of A1:

func A1() {
    A
    await B 
    await C
    await D
    print(“but what about me??”)
    print(“got here”)
}

To satisfy

  1. Waiting (as await does in A1 ) must not block the thread it’s running on. It only blocks its own path of execution.

A1 need to be a coroutine, right?
But it is not marked with async. Does this mean that every function in Swift becomes a coroutine?

2 Likes

If I'm not mistaken, that is what the original post says, a little further down:

AFAIK, the only plausible way of implementing these behaviors is a coroutine, and (no thanks to anything I’ve said or done) that’s what’s actually being proposed, I've been happy to see.

Maybe this is a naive view, but await sniffs like an operator which will be abused more than useful. What I am reading is that you want a way to force async functions to fully complete before returning.
I kind of wanted this the first few times I had to write a completion handler. I stopped having that desire once I thought about what was really happening in these calls, which is when you are crossing into another domain of control. I'm picturing the situation where one requests the contents of a URL, and it doesn't simply timeout as a failure, but delivers a few packets every so often, in aggregate taking many minutes to return. In the meantime the user is shaking their phone screaming "f*#$^%g DO SOMETHING". The server answering the request is going fast as it can, but it's on a remote island and the RFC 1149 connection is having problems.

Is there a scenario where this would be a serious improvement over accepting the needs of the async function?

2 Likes

While async/await would be usable directly where it makes sense, there is also the intent that libraries such as futures or promises would be layered on top. It would then be a simple matter to wrap the async method that will take several minutes in a promise and hang a handler off of it.

I think my writerly tendencies got the best of me.

Would you be willing to give an example of a situation where using await to force an asynchronous function to be synchronous is "better" and couldn't lead to problems?

That's not what await does. The async function still runs asynchronously. What await does is block the caller. This is no different than using a dispatch group and then waiting on it.

2 Likes

Ah, okay. Thanks.