Hello,
EDIT: I make mistakes in this post, and fix them in the following answer.
I've just watched the excellent Leveraging structured concurrency in your applications talk by @FranzBusch at SwiftServerConf, and my interest was raised by the withResource pattern described in the talk:
public struct Resource {
public func interact() async { }
internal func close() async { }
}
public func withResource<ReturnType>(
isolation: isolated (any Actor)? = #isolation,
_ body: (inout sending Resource) async -> ReturnType
) async -> ReturnType {
var resource = Resource ()
let result = await body(&resource)
await Task {
await resource.close ()
}.value
return result
}
I'm immensely interested in such temporary and wrapped access to a resource, because I maintain GRDB which uses this pattern for accessing an SQLite database:
let players = try await reader.read { db in
try Player.fetchAll(db)
}
GRDB has a few differences with the pattern described in the talk: the body function is synchronous, and it is non-isolated. But that's not the topic of this message.
The topic of this message is the ability to build and return non-Sendable types from withResource-style methods:
let nonSendable = withResource { resource in
// Return a NonSendable instance built from resource
}
Support for non-Sendable types is very desirable, because it helps people deal with their existing non-Sendable types. GRDB can't deal with them so far, and this had me write a long documentation chapter about this caveat.
Full of hope, I made experiments with Franz's code.
Since I want to deal with non-Sendable type, I first modified the original code so that the returned value is "sent" back to the caller:
// Now both withResource and body have a "sending" result
public func withResource<ReturnType>(
isolation: isolated (any Actor)? = #isolation,
_ body: (inout sending Resource) async -> sending ReturnType
) async -> sending ReturnType {
var resource = Resource ()
let result = await body(&resource)
await Task {
await resource.close ()
}.value
return result
}
This still compiles fine, as expected (Swift 6 language mode).
I then made an exhaustive list of the possible ways to build a Sendable value out of a Resource. Sendable because I want to put all chances on my side in order to build a non-Sendable value, so I first start by identifying the patterns that work in the simple Sendable case. Exhaustive because region-based isolation has various rules for grouping values, and the exhaustivity should help exploring a maximum of those rules.
// A Sendable, non-isolated type
struct Sendable {
// Resource initializers
init(resource: Resource) { }
init(inoutResource: inout Resource) { }
init(sendingResource: sending Resource) { }
// Won't compile: Sending 'inoutSendingResource' risks causing data races
// init(inoutSendingResource: inout sending Resource) { }
// Factory methods
static func make(resource: Resource) -> Sendable { fatalError() }
static func make(inoutResource: inout Resource) -> Sendable { fatalError() }
static func make(sendingResource: sending Resource) -> Sendable { fatalError() }
static func make(inoutSendingResource: inout sending Resource) -> Sendable { fatalError() }
}
extension Resource {
func makeSendable() -> Sendable {
Sendable()
}
mutating func mutatingMakeSendable() -> Sendable {
Sendable()
}
}
And I tested all of them. Three techniques won't compile. Fine, let's discard them. There are plenty others to live with.
@Suite("Sendable tests") struct SendableTests {
// ❌ Sending 'resource' risks causing data races
func testInitSendingResource() async {
let value = await withResource { resource in
Sendable(sendingResource: resource)
}
print(value)
}
// ❌ Sending 'resource' risks causing data races
func testFactorySendingResource() async {
let value = await withResource { resource in
Sendable.make(sendingResource: resource)
}
print(value)
}
// ❌ Sending 'resource' risks causing data races
func testFactoryInoutSendingResource() async {
let value = await withResource { resource in
Sendable.make(inoutSendingResource: &resource)
}
print(value)
}
}
I then applied all techniques that work with a Sendable type to a non-Sendable type.
And they all fail to compile:
// A non-Sendable, non-isolated type
final class NotSendable {
init(resource: Resource) { }
init(inoutResource: inout Resource) { }
static func make(resource: Resource) -> NotSendable { fatalError() }
static func make(inoutResource: inout Resource) -> NotSendable { fatalError() }
}
extension Resource {
func makeNotSendable() -> NotSendable {
NotSendable()
}
mutating func mutatingMakeNotSendable() -> NotSendable {
NotSendable()
}
}
@Suite("NotSendable tests") struct NotSendableTests {
// ❌ 'inout sending' parameter 'resource' cannot be task-isolated at end of function
func testInitResource() async {
let value = await withResource { resource in
NotSendable(resource: resource)
}
print(value)
}
// ❌ 'inout sending' parameter 'resource' cannot be task-isolated at end of function
func testInitInoutResource() async {
let value = await withResource { resource in
NotSendable(inoutResource: &resource)
}
print(value)
}
// ❌ 'inout sending' parameter 'resource' cannot be task-isolated at end of function
func testFactoryResource() async {
let value = await withResource { resource in
NotSendable.make(resource: resource)
}
print(value)
}
// ❌ 'inout sending' parameter 'resource' cannot be task-isolated at end of function
func testFactoryInoutResource() async {
let value = await withResource { resource in
NotSendable.make(inoutResource: &resource)
}
print(value)
}
// ❌ 'inout sending' parameter 'resource' cannot be task-isolated at end of function
func testResourceMakeNotSendable() async {
let value = await withResource { resource in
resource.makeNotSendable()
}
print(value)
}
// ❌ 'inout sending' parameter 'resource' cannot be task-isolated at end of function
func testResourceMutatingMakeNotSendable() async {
let value = await withResource { resource in
resource.mutatingMakeNotSendable()
}
print(value)
}
}
@FranzBusch, I'm obviously not here to criticize the excellent pattern you have demonstrated in the talk. I was wondering if you have started thinking about building non-Sendable values out of a resource. This does not sound like an unreasonable use case to me.