Execute an asynchronous method for each item in an array, dependently


#1

I'm trying to figure out if there's a functional way to do this in pure Swift, without using something like semaphores or dispatch groups:

I have an array of things, items: [Item], and for each item in the array I want to execute an asynchronous method, func asyncMethod.

I need to execute the methods dependently (so only execute the next item after the previous asyncMethod has called its completion block). If the asyncMethod is ever successful, exit the loop of items early.

I have this working in ugly fashion using DispatchGroup, but wondering if there's a Swiftier way here.


(Yang) #2

import PlaygroundSupport
import UIKit
PlaygroundPage.current.needsIndefiniteExecution = true
extension Sequence {
func asyncEach(_ body: @escaping (Self.Element,_ didFinish: @escaping () -> Void) -> Void, completion: @escaping () -> Void) {
func executeNext(_ iterator: Iterator) {
var iterator = iterator
if let next = iterator.next() {
body(next) {
executeNext(iterator)
}
}else {
completion()
}
}
executeNext(self.makeIterator())
}

func asyncEach(_ body: @escaping (Self.Element) -> Void, completion: @escaping () -> Void) {
    let execQueue = DispatchQueue.init(label: "SerialQueue")
    for e in self {
        execQueue.async {
            body(e)
        }
    }
    execQueue.async {
        DispatchQueue.main.async(execute: completion)
    }
}

}

class Item {
let order: Int

init(order: Int) { self.order = order }

func syncMethod() {
    print("isMainthread: \(Thread.current.isMainThread) \(order) start")
    sleep(1)
}

func asyncMethod(completion: @escaping () -> Void) {
    let order = self.order
    DispatchQueue.init(
        label: "ConcurrentQueue",
        attributes: .concurrent)
    .async {
        print("isMainthread: \(Thread.current.isMainThread) \(order) start")
        sleep(1)
        completion()
    }
}

}

let items = [0,1,2,3,4,5].map( Item.init(order:) )

/*
items.asyncEach({ (item) in
item.syncMethod()
print("isMainthread: (Thread.current.isMainThread) (item.order) done")
}, completion: {
print("isMainthread: (Thread.current.isMainThread) all done\n\n")
})
*/

items.asyncEach({ (item, didFinish) in
item.asyncMethod(completion:{
print("isMainthread: (Thread.current.isMainThread) (item.order) done")
didFinish()
})
}, completion: {
print("isMainthread: (Thread.current.isMainThread) all done\n\n")
})

like this? :thinking::thinking::thinking:


(Anthony Latsis) #3

Here's some approaches. However, this doesn't really scale with Swift's current type system. You can always write a special case like those below that fits best, but you won't be able to further abstract over that to express monadic sequencing in general.

'Brute-force'

class AsyncSequence<T, U> {

  typealias CallBack<T> = (T) -> ()
  
  static func exec(async: @escaping (T, @escaping CallBack<U>) -> (),
                   for sequence: [T],
                   completion: @escaping CallBack<U>) {
    if let _ = sequence.first {
      exec(async: async, for: sequence, index: 0, completion: completion)
    }
  }
  
  static private func exec(async: @escaping (T, @escaping CallBack<U>) -> (),
                           for sequence: [T], index: Int,
                           completion: @escaping CallBack<U>) {
    async(sequence[index]) { response in
      completion(response)
      if (index < sequence.count - 1) {
        exec(async: async, for: sequence, index: index + 1, completion: completion)
      }
    }
  }
}

// Test
func doSomethingIn2Seconds(with number: Int, completion: @escaping (Int) -> ()) {
  Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in
    completion(number)
  }
}

AsyncSequence.exec(async: doSomethingIn2Seconds, for: [2, 3, 4, 5, 6]) { number in
  print(number)
}

Promises

class Promise<T> {
  private var completion: ((T) -> Void)?
  
  func fulfill(_ arg: T) { completion?(arg) }
  
  func then<U>(_ body: @escaping (T) -> Promise<U>) -> Promise<U> {
    var promise = Promise<U>()
    completion = { value in
      let next = body(value)
      next.completion = promise.completion
      promise = next
    }
    return promise
  }
}

// Test
func doSomethingIn2Seconds(with number: Int) -> Promise<Int> {
  let promise: Promise<Int> = Promise()
  Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in
    promise.fulfill(number)
  }
  return promise
}

var promise = doSomethingIn2Seconds(with: 0)

for element in [1, 2, 3, 4, 5, 6, 7] {
  promise = promise.then { number in
    print(number)
    return doSomethingIn2Seconds(with: element)
  }
}