You could hack together something like this to "simulate synchronous coroutines / generators" (though it requires Dispatch for the semaphore, and the finished
variable should be made an atomic variable). Also there is no way to cancel the iterator.
import Dispatch
class Generator<T>: @unchecked Sendable {
let anyGenerator: AnyGenerator<T>
init(_ anyGenerator: AnyGenerator<T>) {
self.anyGenerator = anyGenerator
}
func yield(_ value: T) async {
anyGenerator.nextValue = value
await withUnsafeContinuation { (continuation: UnsafeContinuation<Void, Never>) in
anyGenerator.continuation = continuation
anyGenerator.semaphore.signal()
}
}
}
@globalActor
actor AnyGeneratorActor {
static let shared: AnyGeneratorActor = AnyGeneratorActor()
}
final class AnyGenerator<T>: Sequence {
let semaphore = DispatchSemaphore(value: 0)
var continuation: UnsafeContinuation<Void, Never>?
var nextValue: T?
var finished = false
public init(_ block: @AnyGeneratorActor @Sendable @escaping (Generator<T>) async -> Void) {
Task.detached { [self] in
let generator = Generator(self)
await block(generator)
nextValue = nil
finished = true
continuation = nil
semaphore.signal()
}
}
func makeIterator() -> some IteratorProtocol {
return Iterator(self)
}
public class Iterator<T>: @unchecked Sendable, IteratorProtocol {
private let anyGenerator: AnyGenerator<T>
init(_ anyGenerator: AnyGenerator<T>) {
self.anyGenerator = anyGenerator
}
public func next() -> T? {
if anyGenerator.finished {
return nil
}
anyGenerator.semaphore.wait()
let returnValue = anyGenerator.nextValue
anyGenerator.continuation?.resume()
return returnValue
}
}
}
It can be used like this in synchronous code
let generator = AnyGenerator<(Int, Int)> { generator in
for x in 0..<10 {
for y in 0..<10 {
await generator.yield((x,y))
}
}
}
for point in generator {
print(point)
}