[Pitch ⚾️] Simplifying effects of throwing and asynchronous functions

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

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)? -> type

thrasync-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.

36 Likes

-1 from me (sorry)

I don't think it's a good idea to introduce syntactic sugar for permutations of language keywords, for the simple reason that it doesn't scale. Image Swift will get another keyword that would be used very often alongside try await. We would end up in a very messy state where we have a shortcut for a random set of permutations of keywords, or we would end up having a shortcut keyword for all permutations of those keywords.

8 Likes

I believe this is sufficiently covered by the future directions! We simply keep adding those new combinations, and each one becomes a beautiful Term of Art™.

I would much rather speak quickly and concisely about my thrasyieldinc functions (roughly pronounced /θɹʌˈʃiːldˌɪŋk/) than my throwing... yielding... (hold on let me catch my breath)... async functions.

9 Likes

I see no problem with writing out each keyword individually. It is verbose, but that is a good thing if it improves clarity.

3 Likes

I was just about to say: Please, No!

But then I have just realised It's that time of the year again. :slight_smile:

8 Likes

Damn, you are right :slight_smile:

6 Likes

Tentative +0.5, though I'd really love it if we could hold off until we figure out a way to expand this to unify our messy getter and setter syntax:

struct S {
    var x: Int {
        mutaconsuming _modiread {
             borrow_yield &whatever
        }
    }
}
14 Likes

I know it's time of the year, but would love to have typed effects in the language. :smiling_face_with_tear:

You got me. :sweat_smile:

1 Like

This proposal is a good start, but it does not go far enough.

In particular, although the proposal would replace a large number of occurrences of try, it does not eliminate all of them. The keyword try never should have been added to the language, and it should be removed in its entirety as quickly as possible.

To achieve that goal, I propose that we replace try with the strictly superior spelling do or do not. While this will be the first keyword in Swift to contain spaces, I’m sure the parser can handle it no problem.

23 Likes

+1. I think that feels pretty natural alongside using the force to unwrap optionals :+1:

11 Likes

:joy:

If we are going to go this route, I propose we also add gimmenow keyword when we don't want to wait for something. We're the programmers and we should decide whether something is waited on or whether we get it right now!

13 Likes

Strong +1. Swift has too many keywords. I would go further though and combine the effects keywords with the decl keywords:

thrawaitf f() { ... }
thrart x: T { ... }
thrubscrait(n: T) -> U { ... }

And so forth.

8 Likes

i've been arguing within my team that thrasync should be the default and not require us to type that. i had proposed that we shd use sync to clearly denote when we DON'T want thrasync. But i much prefer your gimmenow keyword for its limpid, crystalline and emphatic clarity.

2 Likes

To be honest, this doesn’t feel like an appropriate venue for an April Fools joke. Especially one not explicitly labeled as such. People may see this in the future and consider it a serious proposal.

4 Likes

Instead of a beautiful conjoining of keywords, I propose we lean into the inference system even more and just have a single keyword: infer. If it can't make a single version of a function or type, it will just make more versions with modern AI solutions!

This condenses all keywords, including isolated, escaping, underscored compiler flags, and all others into one!

Take a look at this absolutely horrendous Task.init parameter:

@_inheritActorContext @_implicitSelfCapture operation: sending @escaping @isolated(any) () async -> Success

Wouldn't it be just more elegant to just:

operation: infer () -> Success

// or even better, infer the type as we are accustomed to:

operation: infer

This saves even more source file bytes at the cost of compilation time, but I think we are all nostalgic for the time we have more time to ourselves while compiling: xkcd: Compiling

3 Likes

I was really excited to read the title and thought "FINALLY" and then got super disappointed reading this. It feels bad to make jokes about the holes in the language that have sat unaddressed for years.

6 Likes

Why not go a little further and just replace all keywords with dwim (do what I mean)?

4 Likes

In other words, vibe effects? I like it.

6 Likes