Dropping the "await" keyword (in some cases?)?

await is a suspension point marker and must always be visible. Like throw it is for the developer benefit, not the compiler (compiler knows it already). That and the viral effect on the code base is one of the distinction / selling points of async/await compared to fiber based implementations (strictly speaking those could also use visual markers / function colouring to denote suspension points, I just haven't seen that done anywhere).

5 Likes

I'd say that there should be even more await markers than now. Example:

await foo() + bar()

as written it's quite vague to me what's going on here. I know I can put the subexpressions in parens:

(await foo()) + (await bar())

but that's quite awkward and perhaps still impossible to represent some of the cases of a promise-based implementation:

foo().await + bar().await // both promises awaited and results added normally
(foo() + bar()).await // the sum of two promises (a promise) is awaited
foo() + bar() // the sum of two promises (a promise) is returned
foo().await + bar() // the sum of a value and another promise is returned
(foo().await + bar()).await // ditto the above, but awaited

OK, I see that the common conception is that one should be explicit here and having to use the await keyword is a good thing.

But: If the creator of an API decides that it should be usable without the await keyword, so he annotates the functions accordingly? Good or bad?

The other thing I asked somewhere else is if the async version of a function should have to be named differently (as it is currently the case), I do not really see a good reason, why not call the async version of forEach also forEach? I really think that at least both requirements make it quite cumbersome to e.g. switch from a synchronous piece of code to an asynchronous one.

What does this mean, exactly? The await keyword is not a matter of API design. It's necessary for the same reason some form of try is necessary for expressions which can throw.

await is necessary because of the effect it has on the caller, not the callee.

4 Likes

The only situation I can imagine where this might apply is something like a function that witnesses an async protocol requirement but is known by the conformer to not actually suspend, ever. It would be okay to call this without await in a concrete context since there's no chance of suspension. But such a type could just provide a synchronous version of the function.

You can apply that argument to any function tagged with async that is known to never suspend.

I would imagine that having a blanket rule so you can reason about the call site without inspecting the body of the called methods greatly outweighs any perceived or actual benefits of being able to exclude it in some scenarios.

After all, the same arguments can be, and have been, made for try.

1 Like

It's also worth calling out the horrible footgun that is "actor reentrancy" + "suspension points". await is the lone defense against this exploding in very bad ways.

3 Likes

This has been my bane for the past week or so. Knowing I'm safe if I eliminate awaitable calls is really a life-saver.

1 Like

Right, I was just trying to envision a situation like @sspringer mentions where "[annotating] the functions accordingly" might be appropriate for the API author to indicate "I know this function is async but it doesn't actually suspend." In most cases I think the response can just be "get rid of the async then," but if it's witnessing a protocol requirement that might not be an option.

In any case, I agree that I don't really feel like this is a problem.

In current Swift, the one place I see await becoming a bit onerous is for orchestration of a complex series of async tasks in a component with no local state. One example of this would be a shell-script-like program (the kind I enable with Shwift: GitHub - GeorgeLyon/Shwift: Shell scripting in Swift). In the body of a script, almost all statements are expected to be async, and the script itself does not have any state that could be accidentally mangled by suspension, nor do we expect reentrant behavior. In these cases await becomes a little noisy and provides little-to-no value.

1 Like

You can do this already:

protocol P {
    func a() async
}

class C: P {
    func a() {
        print("a!")
    }
}

C().a()
2 Likes

Oh cool, I had missed that synchronous functions could witness async requirements!

Hmm. The actual method that is called is not async. But the class fullfills the requirements. ... So the async keyword in the protocol is kind of without meaning so to speak, observed something like this already. I am not really understanding this case. Does the protocol say that asynchronuous implementations are allowed, but not required?

Is this really true though? With SE-0338 it seems like any async function can suspend and even without awaiting anything in its body.

The async keyword in the protocol allows a conformer to use an async method, but it doesn't require one. If you think about it, this makes sense, as a synchronous function is a degenerate case of an async method.

This ability gains more importance when the conformer is an actor. Take this modified example:

protocol P {
    func a() async
}

actor A: P {
    func a() {
        print("a!")
    }

    func b() async {
        print("b!")
    }

    func c() async {
        a()         // no await is necessary because it is invoked internal to the actor
        await b()   // await is necessary because the function is tagged async
    }
}

Task {
    await A().a()   // await is necessary because the call is made from outside the actor
    await A().c()   // await is necessary because the call is made from outside the actor
}

We see the value in allowing a() to be non-async even though it's witnessing an async protocol requirement.

2 Likes

I don't see how SE-0338 changes the argument. It is always true that a function can never suspend anywhere except at a point where another async function is invoked. If this were not so, actors would be completely impractical, as one would be spending more time rechecking invariants than getting useful work done.

What I meant is that even though an async function may not suspend in its body, calling the function might suspend because of SE-0338.

1 Like

What's wrong with calling it with await (even when it known to never suspend)? In that case await is quick anyway. Removing a few characters doesn't worth it.

Bad because he thinks he's too smart for the language to hold his hand but he's wrong.

3 Likes

Good answer.