In converting an existing codebase (not large, but with a variety of completion-handler-based asynchronicity strategies), I ran into a couple of ergonomic issues. I don't recall these being discussed during the pitch phase of Swift concurrency, so I'm offering them as ideas for general discussion here and in related forum threads.
I've run into scenarios which boil down to this fragment:
try await Task {
return try await someAsyncThrowingFunction()
}
which can equivalently be written:
try await Task {
try await someAsyncThrowingFunction()
}
It bothers me that there is so much try await
boilerplate here, relative to the rest of that code. The inner closure's try await
seems like a redundancy.
It seems to me that, at the point where a function or closure is returning a value, it's unnecessary to indicate a suspension point in the return
statement, because:
-
There's no following code inside the function/closure, so knowledge of a suspension point inside the function/closure doesn't benefit anyone reading the code.
-
Presumably, there must be a suspension point in the caller at the point of the call, but it's not likely that readers of the caller's code is going to end up getting confused somehow.
Am I wrong in thinking (a couple of fairly obvious special cases aside, which I'll get to later) that there's no real danger in omitting the await
on a return
statement?
By a similar argument, the try
doesn't seem necessary either. Whether the function/closure exits by returning a value or by throwing, the transfer of control doesn't bypass any code within the function/closure.
If Swift allowed the omission of try await
in this scenario, I think it would eliminate a lot of useless try await
boilerplate that I find myself writing. The result would be something like:
try await Task {
someAsyncThrowingFunction()
}
To my eyes, this is clearly-enough marked.
Note that I'm suggesting this change for all uses of return
, not just at the geographic end of a function's source code. The rule would be that if the return
transferred control out of the function/closure without involving any code at a different location in the source code, those keywords could be omitted.
As special cases where the omission would not be allowed, I can think of two possibilities:
- If the transfer of control involves executing a
defer
block, the keywords should be required. - If the returned expression could throw an error that's caught by a
catch
block, the keywords should be required.
There may be others.
I realize that there are very many Swift developers who strongly prefer not eliding syntax, so the keyword omission would be a stylistic choice (subject to control via linters?).