SE-0430: `transferring` isolation regions of parameter and result values

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.

1 Like