I'm trying to come up with a design for a Task-like primitive for non-sendable and/or non-copyable types, but it's pretty hard to make the compiler happy.
I've made an implementation that produces no warnings but it's suboptimal, to put it mildly.
Here is a simplified version
final class NonCopyableValueBox<T: ~Copyable> {
private var value: T?
init(value: consuming T) {
self.value = consume value
}
func take() -> T? {
let value = value.take()
return value
}
}
actor Future<T: Sendable> {
enum State {
case pending([CheckedContinuation<T, Never>])
case fulfilled(T)
}
private var state = State.pending([])
func get() async -> T {
switch state {
case .pending(var continuations):
return await withCheckedContinuation { continuation in
continuations.append(continuation)
state = .pending(continuations)
}
case .fulfilled(let value):
return value
}
}
func fulfill(_ value: T) {
switch state {
case .fulfilled:
assertionFailure()
case .pending(let continuations):
state = .fulfilled(value)
for continuation in continuations {
continuation.resume(returning: value)
}
}
}
}
public struct NCTask<Success: ~Copyable>: ~Copyable {
private let task: Task<Void, Never>
private let future: Future<CheckedContinuation<NonCopyableValueBox<Success>, any Error>>
public init(priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async throws -> sending Success) {
let future = Future<CheckedContinuation<NonCopyableValueBox<Success>, any Error>>()
self.future = future
task = Task(priority: priority, operation: { [operation] in
do {
let value = try await operation()
let continuation = await future.get()
continuation.resume(returning: NonCopyableValueBox(value: value))
} catch {
let continuation = await future.get()
continuation.resume(throwing: error)
}
})
}
consuming func value() async throws -> sending Success {
let box = try await withCheckedThrowingContinuation { [future] continuation in
Task {
await future.fulfill(continuation)
}
}
guard let value = box.take() else {
fatalError()
}
return value
}
}
I feel like it should be possible to express the same simpler, something like this:
public struct NCTask<Success: ~Copyable>: ~Copyable {
actor Value {
var value: Success?
init(value: consuming sending Success) {
self.value = consume value
}
func take() -> sending Success? {
value.take()
}
}
private let task: Task<Value, any Error>
public init(priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async throws -> sending Success) {
task = Task(priority: priority) {
return Value(value: try await operation())
}
}
public consuming func value() async throws -> sending Success {
try await task.value.take()!
}
}
This code however produces several warnings:
return Value(value: try await operation())
- @isolated(any) value of type 'Success' passed as a strongly transferred parameter; later accesses could race
I can silence it by splitting it into two lines and I can't understand why?let value = try await operation() return Value(value: value) // it's fine
value.take()
- Returning 'self'-isolated 'self.value' as a 'sending' result risks causing data racestry await task.value.take()!
- Returning a 'self'-isolated 'Optional<Success>' value as a 'sending' result risks causing data races
Any ideas on how to satisfy the compiler? Or maybe someone can suggest a better implementation?