A case study for `reasync`

Is it not the same with rethrows? (disregarding for a second that we have them already).

1 Like

no, in fact rethrows works in the complete opposite manner in that you can never generate a non-throwing overload, because we are not allowed to overload on throws.

that of course means that throws is immune to the DocC troubles that async has, but we also didn’t have the analogous “need to migrate Result<T, U>-returning APIs to the new colorful APIs” problem that we had with async, simply due to the order in which these features landed during Swift’s history.

if i remember correctly, allowing overloading on async was an explicit design choice made to accommodate migrating uncolored APIs to colored APIs. i’m not sure if anyone was thinking about how that would impact DocC at the time, as async probably predates DocC by several months.

3 Likes

You mean this won't work?

func throwingFunc() throws {
    print("throwingFunc")
    throw NSError(domain: "", code: 0)
}
func nonthrowingFunc() {
    print("nonThrowingFunc")
}
func proc(closure: () throws -> Void) throws {
    print("throwing version")
    try closure()
}
func proc(closure: () -> Void) {
    print("non throwing version")
    closure()
}
func test() {
    proc { nonthrowingFunc() }
    try! proc { try throwingFunc() }
}
test()

for some reason i always assumed the uniqueness checker (for lack of knowing the right term) was blind to all throws but it seems like it only ignores throws on the function signature itself and not throws in closure arguments.

2 Likes

Almost three years late to this, but in answer to Doug's question about how far a peer macro can get: surprisingly far.

I've written a testing library (swift-test-kit) that provides parallel APIs over Swift Testing and XCTest, and the sync/async duplication was bad enough that I ended up writing these exact peer macros as a standalone package: swift-reasync.

@Reasync applied to an async function synthesizes a sync peer by removing async from the signature, from closure parameter types, and from the body. await is removed everywhere it appears (try await, for await in, standalone await, etc), and async let bindings are converted to regular let. Throwing effects pass through unchanged.

There's also a sibling @ReasyncMembers macro that does the same thing at the type scope. Both macros preserve formatting and comments, so the generated peers read like hand-written code.

The generated peer has to be valid in a synchronous context without any further modifications, so the macros only cover the case where the two versions are identical other than async/await. In practice, the constraint isn't too impactful. If your async function is genuinely just "the async version of a sync function," then there's nothing the macros can't handle.

11 Likes

Great work!

We're still missing this and I from time to time wonder if we should just offer this... IDK about the language steering group's thinking about this nowadays, but I wouldn't fend off such inclusion to the stdlib if you wanted to propose it tbh :slight_smile:

Or it may just keep living in a package, but since the problem comes up so often... might be worth a pitch?

4 Likes

Thanks, Konrad! That's encouraging to hear, especially from you, given how much of Swift's concurrency model you've shaped.

I'd be glad to put together a pitch. Quick scoping question before I post it: Would you suggest I pitch stdlib inclusion directly, or frame it more broadly as stdlib vs a swiftlang-org package, and let the pitch thread decide?

1 Like

Posted the pitch!

1 Like