skip to Main Content

I am trying to create code that processes an array. For the purposes of this question we will assume it is an array of integers. The async function that processes them can either throw or do some work on them and return the values.

I have been having issues collecting the results after I create the task group as there really aren’t many methods that allow me to recover from throwing gracefully. .map() for example requires me to put try in front of the whole thing which means if any of the tasks throw I get out nothing.

In this case I expect on occasion a task will throw. If some task throws I really don’t care about it. I would just like to throw away the result and focus on the ones that don’t throw.

The only way I found to do this is to use nextResult() which thankfully converts the thrown error into an enum that can be handled properly. But the resulting code from using it isn’t great. Is there a more eloquent way to write this code?

func asyncThrowOnOddNumbers(_ num: Int) async throws -> (Int, Int) {
    if num % 2 == 1 {
        throw NSError(domain: "OK", code: 1)
    }
    return (num, num * 10)
}

let numSet = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

let results = await withThrowingTaskGroup(of: (Int, Int).self) { group in
    for num in numSet {
        group.addTask {
            return try await asyncThrowOnOddNumbers(num)
        }
    }
    
    var successDir: [Int : Int] = [:]
    while let workResult = await group.nextResult() {
        switch workResult {
        case .success(let value): successDir[value.0] = value.1
        case .failure(_): continue
        }
    }
    
    return successDir
}
// GOAL print "[2: 20, 6: 60, 10: 100, 8: 80, 4: 40]"
// Stretch goal get array with error or a number "[1: Error(), 2: 20, 3: Error(), 4: 40, 5: Error()..."

2

Answers


  1. Perhaps you should not use a ThrowingTaskGroup, and use a non-throwing one instead.

    Make the child task’s type optional, to represent failed tasks as nil. Then you can use try? instead of try to get an optional.

    let results = await withTaskGroup(of: (Int, Int)?.self) { group in
        for num in numSet {
            group.addTask {
                try? await asyncThrowOnOddNumbers(num)
            }
        }
        return await group
            .compactMap { $0 }
            .reduce(into: [:]) { acc, new in acc[new.0] = new.1 }
    }
    

    If you want the error as well, then your approach of using nextResult is probably the shortest way to do it.

    Login or Signup to reply.
  2. You said:

    Stretch goal get array with error or a number "[1: Error(), 2: 20, 3: Error(), 4: 40, 5: Error()…"

    So, you want a dictionary keyed by an Int, whose value is either an Int or an Error. The logical solution for the “value” would be a Result<Int, Error>. Thus, the resulting dictionary would be a [Int: Result<Int, Error>]:

    func asyncThrowOnOddNumbers(_ num: Int) async throws -> Int {
        if num % 2 == 1 {
            throw NSError(domain: "OK", code: 1)
        }
        return num * 10
    }
    
    func results(for array: [Int]) async -> [Int: Result<Int, Error>] {
        await withTaskGroup(of: (Int, Result<Int, Error>).self) { group in
            for num in array {
                group.addTask {
                    do {
                        return try await (num, .success(self.asyncThrowOnOddNumbers(num)))
                    } catch {
                        return (num, .failure(error))
                    }
                }
            }
    
            return await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
        }
    }
    

    (As an aside, I don’t think it is the responsibility of the called function to create the tuple, but rather do that stuff within the withTaskGroup, so that’s the pattern I’ve employed above. But either way, hopefully you get the idea.)


    FWIW, if you just wanted a [Int: Int] that omits the thrown errors, you can do:

    func results(for array: [Int]) async -> [Int: Int] {
        await withTaskGroup(of: (Int, Int)?.self) { group in
            for num in array {
                group.addTask {
                    return try? await (num, self.asyncThrowOnOddNumbers(num))
                }
            }
    
            return await group
                .compactMap { $0 }
                .reduce(into: [:]) { $0[$1.0] = $1.1 }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search