While migrating a project to the new async features of Swift I constantly needed a mechanism for a synchronization point based of the availability of some value. As far as I can tell, currently there is nothing in the stdlib to cover this. Something like:
public actor AsyncValue<T> {
private var state: State
public init() {
self.state = .waiting
}
public var value: T {
get async {
switch state {
case .waiting:
return await withCheckedContinuation { continuation in
let continuations = [continuation]
state = .valueRequested(continuations)
}
case .valueReceived(let value):
return value
case .valueRequested(let continuations):
return await withCheckedContinuation { continuation in
let continuations = continuations + [continuation]
state = .valueRequested(continuations)
}
}
}
}
public func set(_ value: T) async {
switch state {
case .waiting, .valueReceived:
state = .valueReceived(value)
case .valueRequested(let continuations):
for continuation in continuations {
continuation.resume(returning: value)
}
state = .valueReceived(value)
}
}
}
extension AsyncValue {
enum State {
case waiting
case valueReceived(T)
case valueRequested([CheckedContinuation<T, Never>])
}
}
Basically this is something like an 'AsyncSequence' with only 1 value.
Is it just me, or do you guys also needed something like that while migrating?
Sounds pretty useful to me, I would welcome that in the standard library.
(But… does that code work yet for you? There seem to be some open issues with resuming stored continuations from within actor contexts, that‘s why I’m asking)
This very much looks like an (unstructured) Future to me, with the special feature that you can change the value.
I think the authors of Swift Concurrency have said at multiple occasions that they do not want to provide general Futures. So unless I'm mistaken what you need to do here is to provide compelling examples of why this is needed
I agree, this is pretty much like a Future. I think the authors of the Swift concurrency model are right about not taking Futures as the default mechanism for concurrency since it simplifies 99% of the cases where a handle is not needed.
But sometimes you need a handle: whenever the async event can happen before or after somebody requests the value you need a handle / or when a request for an async value is dependent of another async event that is not part of the current context. That is sometimes the case, and that's why I think an AsyncValue in the stdlib would be worth it.
I encountered the problem while dealing with UI flows where some screen was dependent of the user input of another screen.
I probably just missed that feature of the Task API (which would make this pitch obsolete). Can you provide a hint which Task API would provide that functionality? I kinda lost track since it changed so much...
Thanks - but how do you provide an async value to the Task from outside the closure?
(like the public func set(_ value: T) async part of my example above)