Hello,
Thank you for this new proposal aiming at improving compiler diagnostics regarding non-sendable values.
A few weeks ago, I was wondering if a generic async method that returns the result of an input closure should constraint the result type to be Sendable or not:
// 1. No constraint
public func makeValue<T>(
_ value: @escaping @Sendable () -> T
) async -> T
// 2. Sendable constraint
public func makeValue<T: Sendable>(
_ value: @escaping @Sendable () -> T
) async -> T
The reasoning was muddled by the fact that the continuation resume(returning:)
method does not require its input to be Sendable
, despite the fact that it will cross isolation domains.
At that time, I concluded that my best option was to constrain the return type to Sendable (solution 2), even though the continuation api does not require it.
Now that this proposal updates resume(returning:)
so that it transfers the value, I have a third way to write makeValue
:
// 3. No Sendable constraint, transferring output
public func makeValue<T>(
_ value: @escaping @Sendable () -> transferring T
) async -> transferring T
What do you think?
- Should I prefer this third solution?
- Would it "lock" my implementation to continuations (with difficulties making it evolve in the future, at constant api)?
- Could it create difficulties for callers (can it be less ergonomic to require a closure with a
transferring
output that just constraining T to be Sendable)?
Appendix: a real example of such an api
The kind of method discussed in this question is found in the GRDB library, which provides async methods that asynchronously acquire SQLite connections:
/// - parameter value: A closure that fetches
/// values from the database
public func read<T>(
_ value: @escaping @Sendable (Database) -> T
) async throws-> T
/// - parameter updates: A closure that can
/// read and write from the database.
public func write<T>(
_ updates: @escaping @Sendable (Database) -> T
) async throws-> T
Usage:
let playerCount = try await connection.read { db in
try Player.fetchCount(db)
}
let newPlayerCount = try await connection.write { db in
try Player(name: "Chiara").insert(db)
return try Player.fetchCount(db)
}
I was auditing those methods for Swift concurrency when I started wondering if T
should be Sendable
or not.
SE-0430 transferring
just adds a new option.
I want to design an api that can be compiled in the Swift 6 mode, is ergonomic for the users (i.e. they don't even have to think about it), and last a few years, without breaking changes.