In the spirit of other recent proposals that aim to simplify the concurrency model in Swift, I'm excited to bring this idea to the community to smooth out some of the rough edges on declarations and invocations of throwing asynchronous functions. Here's the link to the full pitch, which is also pasted below!
Simplifying effects of throwing and asynchronous functions
- Proposal: SE-NNNN
- Authors: Tony Allevato
- Review Manager: TBD
- Status: Awaiting review
- Upcoming Feature Flag:
SimplifiedFunctionEffects
- Implementation: swiftlang/swift#0401
Introduction
This proposal aims to simplify the declaration and invocation of throwing
asynchronous functions by introducing a new trawait
expression modifier and
thrasync
function effect.
Motivation
Asynchronous functions are very frequently used to represent operations that
involve external resources. Not only are these operations potentially
long-running, but they must be prepared to handle conditions that would cause
the operation to fail unexpectedly—for example, the loss of a connection to some
database. As such, functions that are both throws
and async
are
extremely common.
Because of this commonality, Swift code very often contains the same pairs of
keywords: async throws
effects on function declarations and try await
modifiers on the expressions that call them. Furthermore, remembering which
order these two keywords should be written can be very difficult.
With one of Swift's main goals being to reduce ceremony in the common case, we
believe this situation can be improved with the introduction of three new, concise
keywords.
Proposed solution
We propose adding three new keywords to the Swift language: trawait
, thrasync
,
and rethrasync
.
The trawait
keyword will act as syntactic sugar for try await
. The following
two expressions would be equivalent:
let content = try await readFile()
let content = trawait readFile()
The thrasync
keyword will act as syntactic sugar for async throws
. The
following declarations would be equivalent:
func readFile() async throws -> Data { ... }
func readFile() thrasync -> Data { ... }
Similarly, the rethrasync
keyword would be used in place of async rethrows
.
These keywords compose naturally with typed throws
as one would expect:
func readFile() thr(IOError)async -> Data { ... }
Upcoming language mode
Since the new keywords are more concise and clearly superior, we propose
deprecating the pairs async (re)throws
and try await
in the next
language mode. If the user enables the upcoming feature flag
SimplifiedFunctionEffects
in the current language mode, they will receive
warnings when using the old pairs:
main.swift:5:15: warning: 'try await' is deprecated; replace with 'trawait'
5 | let content = try await readFile()
| `- warning: 'try await' is deprecated; replace with 'trawait'
main.swift:10:17: warning: 'async throws' is deprecated; replace with 'thrasync'
10 | func readFile() async throws -> Data {
| `- warning: 'async throws' is deprecated; replace with 'thrasync'
When enabling the next language mode, these warnings would become errors.
Detailed design
The Swift grammar will be updated as follows:
expression → (try-operator | await-operator | trawait-operator)? prefix-expression infix-expressions?
trawait-operator → trawait
function-signature → parameter-clause (async | throws-clause | thrasync-clause)? function-result?
function-signature → parameter-clause (async | rethrows | rethrasync) function-result?
closure-signature → capture-list? closure-parameter-clause (async | throws-clause | thrasync-clause)? function-result? in
initializer-declaration → initializer-head generic-parameter-clause? parameter-clause (async | throws-clause | thrasync-clause)? generic-where-clause? initializer-body
initializer-declaration → initializer-head generic-parameter-clause? parameter-clause (async | rethrows | rethrasync) generic-where-clause? initializer-body
function-type → attributes? function-type-argument-clause (async | throws-clause | thrasync-clause)? -> typethrasync-clause → thrasync | thr '(' type ')' async
Initially, the parser will continue to accept try await
and async (re)throws
in order to retain compatibility with legacy code. When the
SimplifiedFunctionEffects
upcoming feature flag or the next language mode is
enabled, these will be diagnosed as warnings or errors, respectively.
Introducing new contextual keywords into expression contexts requires special
care, because they may collide with identifiers present in existing Swift code.
As with the unsafe
expression modifier keyword, we require that the trawait
keyword not be separated from the subsequent token by a line break:
// This is a 'trawait' expression.
let x = trawait f()
// This assigns the identifier 'trawait' to 'x', then invokes 'f()' and
// discards the result.
let x = trawait
f()
Source compatibility
This proposal is an additive-only syntax change in the current (Swift 6)
language mode.
Clients will need to migrate their code to replace uses of try await
and
async throws
before adopting the next new language mode. We may provide a
migration tool for this, but really the following shell commands are close
enough:
sed -e 's/try await/trawait/g' *.swift
sed -e 's/async rethrows/rethrasync/g' *.swift
sed -e 's/async throws(\([^)]\))/thr\1async/g' *.swift
ABI compatibility
This proposal has no impact on ABI.
Future directions
We expect that the Swift language may add more function effects in the future;
for example, coroutine-based generator functions could be represented as
func generate() yielding -> Element
. It is a perfectly reasonable extension
of this concept to have throwing and/or asynchronous versions of these
generators. If that happens, new keywords to handle the possible combinations
follow quite naturally:
func generate() yielding -> Element { ... }
func generate() thrielding -> Element { ... }
func generate() asyieldinc -> Element { ... }
func generate() thrasyieldinc -> Element { ... }
Alternatives considered
We considered other possible spellings for these keywords, such as awaitry
and
asythrows
. However, the authors consider trawait
and thrasync
to be the
most phonetically pleasant choices.
Another alternative would be to do nothing. However, we believe that the savings
in source file bytes is well worth making this change.