SE-0296: async/await

What is your evaluation of the proposal?

I started off as broadly +1, but on more careful reading of the proposal and allowing things to stew a bit, I'm not so sure. It's quite hard to judge without also considering the planned executor/continuation API (which I haven't looked at in as much detail). But even considering the async/await model proposed in this document as a kind of abstract concept, I have concerns with the programming model that I think merit further discussion.

  1. Read-only async properties and subscripts should be allowed. The proposal says:

    Properties and subscripts that only have a getter could potentially be async ... Prohibiting async properties is a simpler rule than only allowing get-only async properties and subscripts.

    My argument against this is similar to Doug's argument against banning inheritance for actors: why impose arbitrary restrictions? Also, currently, functions do not satisfy get-only property requirements in protocols, meaning synchronous types which want to use properties for a more natural interface will also be forced to use functions.

    protocol AsyncCollection {
      func startIndex() async -> Index
    }
    struct MySyncCollection: AsyncCollection {
      // Does not conform to AsyncCollection:
      var startIndex: Index { ... } 
    
      // Instead, they'd have to write this:
      func startIndex() -> Index { ... }
    }
    
    let syncInstance = MySyncCollection()
    syncInstance.startIndex() // function interface forced upon non-async code
    

    Allowing async properties might just be the best interface for both synchronous and async conformers.

  2. Cancellation.

    The proposal tries to defer this discussion, but I believe it is a crucial element of the async/await programming model to say whether or not async functions can be cancelled. If they can be cancelled, it means every possible suspension point is also a possible return point, which is a critical detail that the proposal does not mention at all.

    For example, consider the following code:

    let lock = await getLock()
    let otherData = await downloadOtherData()
    lock.release()
    

    If async functions could not be cancelled, we know that either lock.release() will be called, or that the function never got scheduled again and leaked its whole stack (lock.deinit will never be called). But if they can be cancelled, lock may be destroyed without us calling .release(). Is that obvious from the code? I don't think it is.

    There are potential dragons here if you leave data in an invalid state across an await, and that data is somehow visible to others or (the more subtle case) relied upon in the type's deinitializer. For example, if lock was a buffer that we were filling, we might try to deinitialize invalid memory because we forgot that await meant the function could return in the middle of initializing. And to be fair, when the code looks like: let otherData = await downloadOtherData(), it's hard to spot that there's a hidden return in there. The only clue is on the right-hand side of an assignment operator.

    As far as I can tell, this possible footgun is the same reason _modify still isn't an official language feature (because every yield is also a possible return point). It's important we don't ignore this very important consideration, which developers using async/await in other languages can struggle with.

Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?

It's fine, I guess. I'm a little concerned that we're doing this without a lot of the infrastructure other languages have (coroutines, generators).

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I've mostly been using Swift since async really started taking the world by storm, so I've missed a lot of that. That said, I have heard criticisms from other developers that async ends up spreading to all areas of your code, and I could see similar things happening here. For example, with synchronous functions able to satisfy async protocol requirements, I could imagine lots of developers will default to making everything async. I'm not sure there is any way to avoid that, or what a better approach would be.

9 Likes