Home > Software design >  Implementing a custom asynchronous sequence in Swift
Implementing a custom asynchronous sequence in Swift

Time:07-14

Imagine I want to create a function that, given an array of numbers, computes the square, cube, and fourth power of each number in an asynchronous fashion and returns a flattened, asynchronous sequence of all these results.

So, for example, for the input array [2, 3, 4], it should return an AsyncSequence instance yielding the elements [4, 8, 16, 9, 27, 81, 16, 64, 256].

Then let's say, instead of computing x^2, x^3, x^4, I would like it to compute x, x^2, x^3, ..., x^k where k is sort of a random integer that can be different for every x and is not known beforehand (its value comes to be known only as the powers are being computed). How would I implement such a pattern?

CodePudding user response:

An AsyncStream could do the job. E.g., given an array of integers, values, the asynchronous sequence would be:

let stream = AsyncStream<Int> { continuation in
    Task.detached {
        for value in values {
            var result = value
            for _ in 1 ..< n {
                result *= value
                continuation.yield(result)
            }
        }

        continuation.finish()
    }
}

But this calculation of x², x³, ..., xⁿ for each element in the input array might not be a good candidate for an asynchronous sequence. Each subsequent value can be calculated nearly instantaneously (just multiplying the previously emitted value by x) and, as such, should probably just be a standard, synchronous sequence.

Generally, asynchronous sequences should be those that are sufficiently slow to justify moving it into the background or otherwise has results that are emitted asynchronously over time.

CodePudding user response:

Thanks a lot to Rob for providing the basic idea on how to implement something like this.

I wrote it in the following way:

func powers(of numbers: [Int]) -> AsyncStream<Int> {
    return AsyncStream<Int> { continuation in
        Task {
            for number in numbers {
                for await power in Powers(of: number) {
                    continuation.yield(power)
                }
            }
            continuation.finish()
        }
    }
}

struct Powers: AsyncSequence {
    init(of base: Int) {
        self.base = base
    }
    
    func makeAsyncIterator() -> PowersIterator {
        return PowersIterator(base: self.base)
    }
    
    let base: Int
    typealias Element = Int
}

struct PowersIterator: AsyncIteratorProtocol {
    mutating func next() async -> Int? {
        if !self.shouldFinish() {
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            defer {
                self.exponent  = 1
            }
            return power(self.base, self.exponent)
        } else {
            return nil
        }
    }
    
    private func shouldFinish() -> Bool {
        return Int.random(in: 1...10) == 1
    }
    
    private func power(_ base: Int, _ exponent: UInt) -> Int {
        return (0..<exponent).reduce(1) { power, _ in power * base }
    }
    
    var exponent = UInt(1)
    let base: Int
}

It can be invoked using this code:

Task {
    let numbers = [1, 2, 3, 4, 5]
    for await power in powers(of: numbers) {
        print(power, terminator: " ")
    }
}

Possible output:

1 1 1 2 4 8 16 32 64 3 9 27 4 16 64 256 5 25 125 625 3125 15625

The solution is a little more complex than it ought to be. But that's of course because I actually wanted to compute something that would really need to be computed asynchronously and has the same computational structure as this example. That is also the reason for why I created a separate async sequence for computing the powers.

If this helps anyone out, I'll be glad.

  • Related