[Concurrency] Asynchronous functions

I imagine it is similar to other uses of the async/await keywords in other languages, it makes it clear that asynchronous work is done in that function.

Plus from an API consumer point of view, you may not know the implementation of a method and what it is doing, knowing it has asynchronous calls which may be suspended

The fact that you have to await the function, or use it with a local async variable, should be enough to make that distinction. It's the same reason we don't end methods marked with throws with Throwing.

15 Likes

In what sense? The compiler telling you?

It’s the same case with throws, if you can only see the public declaration then you know that there is the chance of the function calling asynchronous code.

I guess the difference is that with throws you can override catching/failing gracefully with try! for asynchronous functions you can’t override that...

Maybe this can continue over [Concurrency] Asynchronous functions? I think it's getting out of a roadmap thread.

(Moderator note: this post and the chain of posts it responds to were originally in the roadmap thread)

1 Like

please make it

async func hello() -> String

It's far too hidden at the end of the function name, and unlike throws it significantly affects the choice to call it over a sync function

1 Like

I don't think we should allow overloading between sync & async functions. It does close off the possibility of conversion between sync and async function. All of the APIs I've seen have sync version just wait for async APIs to finish, which I think is better explicit:

task1()
Task.syncWait {
  await task2()
}
task3()

So I don't think [allowing sync & async overload] is a good tradeoff for [sync & async conversion].

I really like the choice of ‘async’ at the end, it’s describing an attribute to how the method returns its type, just like ‘throws’. It’s consistent with the rest of the language. I would encourage anyone arguing to put it as a prefix to think about how a method signature is constructed in Swift and not just pull from other languages because that’s what they are used to.

4 Likes

It's part of the function's type. Placing it after the parentheses emphasizes that and keeps it uniform with throws and rethrows and closure definitions:

func f (x: Int) async -> Int { ... }
let g = { (x: Int) async -> Int in ... }
// f and g have type: (x: Int) async -> Int
7 Likes

Posted jointly with @dabrahams, @compnerd.

Swift already uses coroutines today (i.e. _read, _modify, and yield). While we're quite excited about further coroutine-based development, we'd expect to see more of discussion on the relationship between async/await and Swift's existing coroutines, and how they might be unified.

Thank you very much for your hard work on this!

10 Likes

So today, we have functions that largely perform an operation, however some subset of callers may be interested in more detail about what was done. This pattern is common enough that we have a keyword for it: @discardableResult.

In async world, we imagine that tasks run longer than synchronous functions, and so the part that builds the result may be longer in like proportion. However, if many callers don't need the result, they may become disinclined to pay for what may be a nontrivial task. This happens for sync code already, but is going to happen more with async.

Consider a typical application HTTP request with signature

func make<T: Type>(request: URLRequest) async throws -> T

Many callers may be satisfied to know we got some response, and perhaps that the headers said it was 200 OK. Your file was saved, your message was sent. Some individual caller may want to download all 2GB of response and parse it into JSON. The cost of doing the latter for a caller who wanted only the former is nontrivial. Yet a third case might be simply firing off the response and not waiting for the result at all.

Arguably, this is a job for different functions:

func make<T: Type>(request: URLRequest) async throws -> T
func make(request: URLRequest) async throws
func make(request: URLRequest) //no-await or fire-and-forget case

Beyond tripling the API surface and having to remember which version to call, and keeping track of whether the third "non-async" function waits for the response or returns immediately before the request is even made, there are several practical problems:

  1. Callers have to disambiguate these somehow. Ordinarily we expect to be able to bind a value to a function call and infer the value's type, but here the first 2 functions differ by return type, so type inference is not available. In addition, the latter 2 functions have the same return type, so I'm not even sure how to disambiguate them.
  2. Writing and maintaining these functions is tough. Ideally one function would call the others, but it's difficult to see how for these 3. If we write the second to call the first we do extra work. If we write the first to call the second, it's unclear how to build the result. Maybe there's some common task we can factor out into a private function detail used by everyone, but it's highly specific to each problem.

There are some other ways to do this like adding parameters etc. but I think these problems are a) likely to be pretty common for async code and b) likely to cause "ceremony" for both clients and implementors, which is the kind of thing we are trying to avoid by introducing async in the first place.

The obvious way to handle the situation for the non-async third case is to allow launching a new coroutine through an explicit syntax like nonawait makeRequest(myRequest). This produces the fire-and-forget version and so we can eliminate dealing with that function. There are some details to wrestle with here (what's the qos of the new coroutine?) so it may be better-suited for a platform function than a language keyword.

In terms of the remaining 2 await cases, one solution would be to add a control flow primitive to the called context, reflecting if the calling context will discard the result. That is, (spitballing syntax)

@disardableResult func make<T: Type>(request: URLRequest) async throws -> T {
    let result = try await request.make()
    if result.code != 200 { throw Errors.BadResponse(result.code) }
    //return early if caller does not use return value.  
    //Since value is unused we can avoid specifying any value for this statement
    return @discardableResult 
    //caller wants the value; go to the bother of making one
    return try T(parsing: await result.receiveData())
}

This sort of control flow would unify the first 2 functions into a single definition that will work for either kind of caller, sidestepping all the practical problems discussed prevoiusly.

Some legacy code may want advance notice about whether the result is discarded. For example, an HTTP library may expect to know whether or not we want the full response, at the time the request is made, to pick an allocation size etc. So maybe we want an alternative or additional control flow primitive:

@disardableResult func make<T: Type>(request: URLRequest) async throws -> T {
    if #discardableResult {
        try await request.make(response: false)
        return @discardableResult
    }
    return try T(parsing: await request.make(response: true))
}

I realize some of this is well outside the scope of the pitch, but I think these issues are likely to arise pretty quickly as we adopt async, and it would be nice to have the tools to keep the implementation manageable.

on overload resolution


func doSomething() -> String { ... }
func doSomething() throws -> String { ... } // not allowed in swift 5 and below
func doSomething() async -> String { ... } // okay 
func doSomething() async throws -> String { ... } // okay?

async is a subtype of async throws. If I were to hazard a guess, I'm gonna go with:

  • none & async
  • none & async throws
  • throws & async
  • throws & async throws

I think we can make do with one (and a half):

func make<T: Type>(_: T.Type, ...) async throws -> T {
  if T.self == Void.self {
    // No payload, and it's won't warn on unused result
  } else {
    // Some payload
  }
}

// For some QoL
func make(...) {
  make(Void.self, ...)
}

// wait for completion
await make(...)

// Don't wait, please don't do this, at least set a timeout
Task.runDetached {
  await make(...)
}
1 Like

They are completely semantically different features and cannot be "unified". We have very intentionally not approached coroutines with the goal of providing a maximally generic feature for library programmers, like some languages have, because we feel that the needs of the different language features that would take advantage of them are broadly different.

Asynchronous functions only use "coroutines" at an implementation level, and I fear that that sentence is probably contributing to the extremely broad misunderstanding of what coroutines are. I will fix the proposal to remove it.

18 Likes

This is an intriguing idea, but one issue here is converting Void to some protocol Type to return. That would require implementing Type on Void. The implementation is probably is just a lot of fatalErrors on Void, the otherwise real type you can instantiate and pass to things.

However this does make me think of a related idea, namely using Never in lieu of Void here. That currently isn't allowed, nor is returning an instance of it (which is required for this "non-void" function returning T), but maybe we could adjust some of these restrictions specifically for discarded values.

It's a very interesting idea, though I think we can separate it into a thread on its own. This seems to apply equally well to sync case.

1 Like

Just to add to this, _modfy as of right now is also an implementation detail of the standard library and not a public language feature. So there is even less reason to expect it to be discussed in this proposal.

7 Likes

Well, it isn't public, but it's a feature that's been pitched in this forum with 287 replies, so I think it's certainly reasonable to ask how a feature that's already implemented and pitched for public consumption dovetails with this one.

3 Likes

Perfectly reasonable to ask for thoughts on it in this thread. Not reasonable to expect it in the proposal.

2 Likes

This proposal is really awesome, I'm very much looking forward to this!

Some random thoughts:

  • The proposal to force ordering of async before throws is interesting - I don't think we have any precedent for this. I am not opposed to it, just making an observation. Are you force the same thing on the expression side, rejecting try await foo()? It seems like it should be consistent.

  • I agree with the comment by @Jay-Madden that it would be good to consider what a naming convention would look like here. Something should be added to the API design guidelines about async.

  • I don't understand how the overloading rules will work. The existing try marker doesn't influence type checking (it is verified after the expression is checked), so this is deviating from its design. There are also ambiguous cases, given that the await marker covers a whole subexpression, for example in await (x() + y()) if both x and y have both overloads, then either could be picked. I suppose the rule is to pick a maximally awaiting version of this? How does this work if the overloads return different types etc? This seems like it will just make the type checker a lot more complicated.

    Async/await are new features, so I don't see any reason why Swift programmers would want to write overloads like this (just like they don't/can't write overloads of throwing functions). I think this issue is limited to the Clang Importer for the ObjC feature, and that. can be handled by adding an Async suffix to the imported method (potentially iff there is a conflict). I would recommend going that way so the core language stays simpler and there is less pressure on the already complicated type system.

  • Relating to the above, there is clearly a subtype relationship between async and sync functions of the same type. The rationale in the discussion about function types trying to explain why there are no implicit conversions doesn't make much sense to me.

  • Closure expressions have a "lot" of sugar (citation needed), and I think it is important for this to work consistently with throws in closure expressions. Should we allow { async in ...} as sugar for "infer the signature, but make it async"? I'm not actually sure if that works with throws closures - I sort of hope not :-)

  • You will either want closure expression to infer their async'ness from callers, just like they infer their @escaping marker, or you will want subtyping between async and non-async functions, so that things like this work: functionThatTakesAnAsyncClosure { $0+1}. The closure implementation doesn't "need" async, but needs to be compatible with a function argument declared as async.

  • I agree that taking await as a keyword is the right way to go.

This proposal is super awesome, thank you for driving this forward. My concerns about the subtyping and overload resolution issues here are my most important. concerns. I would love to know if you've thought about other alternatives here that would make this more consistent with how try and throws works in the language.

Specific question for you: what does the Clang importer do today when you have an objc method that takes an NSError (importing as throws) and another one that does not? A naive import would turn them into a redefinition, does one get renamed? If so, can we do the same thing for completion handlers? Did you consider just "not" importing them in this case? None of these auto-imported things are required for compatibility anyway, they are a new thing.

-Chris

10 Likes

A commonly used pattern today is to pass a (queue, completion handler) pair which provides the target queue context for the handler execution. This context is not necessarily the same one as the caller is executing on. Is that possible with async/await? If not, that means developers are going to have to double-dispatch as the completion handler will first execute on the wrong context.