Bram
Bram

Reputation: 3283

Async await list detail

I'm trying to fetch a detailed list of items through a multi step async call. First I fetch a list of identifiers, after which I fetch the details for each of these identifiers.

func fetchList() async -> [UUID] {
    // some async network call here
    await Task.sleep(2)
    return [UUID(), UUID(), UUID()]
}

func fetchDetail(forUUID uuid: UUID) async -> String {
    // some async network call here
    await Task.sleep(1)
    return "\(uuid.uuidString.reversed())"
}

Individually these function work fine, but I'm stuck when I want to try and use a .map on the list.

Task {
    let list = await fetchList()
    let details = await list.map { fetchDetail(forUUID: $0) } // Does not work
}

I did find out about the withTaskGroup function and I could of course append to an array of items asynchronously, but I was wondering if there is a better way to achieve this functionality.

Upvotes: 1

Views: 589

Answers (2)

matt
matt

Reputation: 536027

In the example you've actually given, fetchList returns an array of values, all at once. The fact that it is asynchronous is therefore effectively irrelevant. The point is that we are now starting with an array and we wish to process its elements by calling an async method.

You thus have basically two choices: you can process the elements one by one, waiting for each, or you can process them concurrently. If you wish to process them concurrently, that is what a task group is for, and you claim to know about that already, so there is no more to be said. This is exactly the kind of example given by the WWDC video on this topic, as well as my tutorial at https://www.biteinteractive.com/swift-5-5-asynchronous-looping-with-async-await/.

Note that, as pointed out both in the video and in my tutorial, if you choose to process the values concurrently, the results can come back in any order, so you have to take some extra steps if you don't want to lose the connection between each original URL and its processed result. The usual solution is to construct a dictionary or just a tuple of pairs. In my book, I use the dictionary approach. Adapted from that code, you could do something like this:

func processManyURLs(urls: [URL]) async -> [URL:String] {
    return try await withTaskGroup(
        of: [URL:String].self, 
        returning: [URL:String].self) { group in
            var result = [URL:String]()
            for url in urls {
                group.addTask {
                    return [url: await fetchDetail(url: url)]
                }
            }
            for try await d in group {
                result.merge(d) {cur,_ in cur}
            }
            return result
    }
}

Upvotes: 2

Paul B
Paul B

Reputation: 5125

Map for async code needs AsyncSequence to work on.

struct DelayedUUIDs: AsyncSequence {
  typealias Element = UUID

  struct DelayedIterator: AsyncIteratorProtocol {
    private var internalIterator = [UUID(), UUID(), UUID()].makeIterator()

    mutating func next() async -> UUID? {
      await Task.sleep(1_000_000_000)
      return internalIterator.next()
    }
  }

  func makeAsyncIterator() -> DelayedIterator {
    DelayedIterator()
  }
}

func fetchList() async -> DelayedUUIDs {
    // some async network call here
    await Task.sleep(1)
    return DelayedUUIDs()
}

func fetchDetail(forUUID uuid: UUID) async -> String {
    // some async network call here
    await Task.sleep(1)
    return "ID: \(uuid.uuidString)"
}


Task {
    let list = await fetchList()
    let details = list.map({ id in await fetchDetail(forUUID: id) })
    for await value in list {
      print(Date())
      print(value)
    }
    for await value in details {
      print(Date())
      print(value as String)
    }
}

UPD: Generic AsyncSequence sample.

struct DelayedArr<E>: AsyncSequence, AsyncIteratorProtocol {
    typealias Element = E
    var current = 0
    var elements: [E]
    mutating func next() async -> E? {
        defer { current += 1 }
        await Task.sleep(1_000_000_000)
        guard current < elements.count else { return nil }
        return elements[current]
    }

    func makeAsyncIterator() -> DelayedArr {
        self
    }
}

let sequence = DelayedArr(elements: [1, 2, 3 ,4, 5])
func asyncIntToString(i: Int) async -> String {
    await Task.sleep(2_000_000_000)
    return String(i)
}

let asyncMapSequence1 = sequence.map{ await asyncIntToString(i: $0)} //asyncronous code
let asyncMapSequence2 = sequence.map{ String($0) } // or syncronous code

Task {
    for await value in asyncMapSequence1 {
        print(Date())
        print(value)
    }
    for await value in asyncMapSequence2 {
        print(Date())
        print(value)
    }
}

Upvotes: 2

Related Questions