I've been pondering upon the following code for a function.
public extension Database {
func transaction<T>(_ closure: @Sendable @escaping (Database) async throws -> T) async throws -> T {
try await self.transaction { db -> EventLoopFuture<T> in
let promise = self.eventLoop.makePromise(of: T.self)
promise.completeWithTask{ try await closure(db) }
return promise.futureResult
}.get()
}
}
This is actual code from the Vapor framework.
Here is the URL for reference: https://github.com/vapor/fluent-kit/blob/main/Sources/FluentKit/Concurrency/Database%2BConcurrency.swift
In this transaction function, the argument closure has the @Sendable and @escaping attributes.
I am wondering if both could be removed.
In particular, the @Sendable attribute implies that the types passed down to the closure must be of Sendable,
which imposes quite strict limitations.
Hence, if we could omit it, that would be much more convenient.
The reason I think it could be removed is because,
while this closure is indeed passed onto the event loop thread,
by the time it leaves the transaction function,
it is awaiting EventLoopFuture.get(),
ensuring that the function invocation of closure is complete.
In other words, there is no chance of closure being simultaneously called from two different threads.
Indeed, theoretically since the eventLoop type is abstracted to any EventLoop,
implementing a way to store the closure passed to completeWithTask into a global variable or something similar is possible,
but that would clearly be unnatural behavior for EventLoop and EventLoopFuture and I don't think there's anything to worry about.
Furthermore, I think @escaping can be eliminated for the same reason. The closure doesn't actually escape.
Could the above thoughts be right?
I'd love to hear thoughts from someone with more insight to weigh in.
For reference, the specific implementation would look as follows:
public extension Database {
func transaction<T>(_ closure: (any Database) async throws -> T) async throws -> T {
try await withoutActuallyEscaping(closure) { (closure) in
let closureBox = UncheckedSendableBox(closure)
return try await self.transaction { db -> EventLoopFuture<T> in
let dbBox = UncheckedSendableBox(db)
let promise = self.eventLoop.makePromise(of: T.self)
promise.completeWithTask {
let db = dbBox.value
let closure = closureBox.value
return try await closure(db)
}
return promise.futureResult
}.get()
}
}
}
And I believe this thought could be generalized.
I refer to these functions that take a value and allow for flexible processing using a closure, as scope functions.
Such scope functions can indeed be asynchronous, however, even when they're asynchronous,
in my opinion, as long as the closure execution in the scope functions is done serially,
they don't necessarily have to be @Sendable or @escaping.
What are your thoughts about this?