2

Is there a Combine operator that will run a series of futures in sequence, running each to completion before starting the next?

I can do this with the very messy:

f1
.flatMap { _ in 
  f2
}.flatMap { _ in 
  f3
}.flatMap { _ in 
  // ... 
}

but I'd prefer something like:

sequence(f1, f2, f3, ...)

In some frameworks, this would look like:

f1.then { f2 }.then { f3 }
Bill
  • 44,502
  • 24
  • 122
  • 213
  • It deprnds what “then” means. If you don’t want to feed a value from one future to the next, and if the types are comparable, you can use `append`. Otherwise yes, `flatMap` Or `switchToLatest` is how to serialize. I don’t see why that’s an issue. – matt Mar 07 '20 at 22:25
  • @matt I looked at `append`, but it adds new elements to a publisher, rather than appending new publishers. `switchToLatest` will interleave events, so I think `flatMap` is my best bet. – Bill Mar 07 '20 at 22:35
  • 1
    It doesn't add "new elements to a publisher", it lets one publisher run and then, when it finishes, it lets the other publisher run. So they do indeed publish completely sequentially. – matt Mar 07 '20 at 22:49
  • Likely duplicate of https://stackoverflow.com/questions/59743938/combine-framework-serialize-async-operations perhaps; a lot of interesting answers there. – matt Mar 07 '20 at 22:51
  • And see also my use of `flatMap` here: https://stackoverflow.com/questions/60416549/using-combines-future-to-replicate-async-await-in-swift/60418000#60418000 – matt Mar 07 '20 at 22:52
  • @matt You're right - there's an `append` in the `Publisher` protocol that takes another `Publisher`. My mistake. – Bill Mar 07 '20 at 22:59
  • 2
    That's actually the real `append`. The others are just conveniences. – matt Mar 07 '20 at 23:03

1 Answers1

1

The key is to wrap the Future in a Deferred so it won't execute until it's time:

let f1 = Deferred { Future<Int, Error> { result in

    }
}
let f2 = Deferred { Future<Int, Error> { result in

    }
}
let f3 = Deferred { Future<Int, Error> { result in

    }
}
let jobs = f1
    .append(f2)
    .append(f3)

cancellable = jobs.sink(receiveCompletion: { (completion) in
    print("all three jobs done or one errored.")
}, receiveValue: { value in
    print("value of job:", value)
})

In response to further questions in the comments:

You cannot rely on Deferred to defer closure execution until a subscriber comes in, because Deferred is a struct and would cause a new Future to be created every time there is a new subscriber.

That's precisely the point of deferred. To create a new Future every time there is a new subscriber and not before then. Another option would be to create the futures inside the appends.

let jobs = Future<Int, Error> { result in }
    .append(Future<Int, Error> { result in })
    .append(Future<Int, Error> { result in })

But then all three futures will execute their code at the same time. I'm assuming you don't want this.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • Thanks! I was considering this, but I saw the following note in the book "Combine: Asynchronous Programming with Swift": "You cannot rely on Deferred to defer closure execution until a subscriber comes in, because Deferred is a struct and would cause a new Future to be created every time there is a new subscriber" – Bill Mar 08 '20 at 23:53
  • What is sequentializing the events here is the `.append`, not the `Deferred`. To put it another way, if you're going to use `.append`, you don't _need_ to use `Deferred`. And we already brought up chained `.append` in the comments and links on the question itself. – matt Mar 09 '20 at 00:10
  • @matt I think @daniel-t is trying to accommodate the fact that `Future`s start running right away, instead of waiting for a subscriber like other publishers. Just appending a bunch of `Future`s will start them all simultaneously, instead of running them one after the other. – Bill Mar 09 '20 at 00:21
  • I extended the answer to cover the extra questions in the comments. – Daniel T. Mar 09 '20 at 00:22
  • @Bill The way `A.append(B)` works is that it _does not even subscribe_ to publisher B until publisher A has published. So Deferred has no effect whatever on how setting up a chain of `.append` behaves. If forming the Future _in the first place_ causes the function to start running, okay, then use Deferred to prevent that. But that has nothing to do with the original question, it has to do with your Future _itself_. – matt Mar 09 '20 at 01:15
  • 1
    @matt Yes, the mere creation of a Future causes its closure to be called. If you don't want a future's closure to run until after some other publisher completes, you have to use Deferred. The question was about how to defer execution of a Future until after some other Future has completed execution. Using Deferred to stall execution directly addresses this question. – Daniel T. Mar 09 '20 at 01:23
  • Yes, that's certainly a true fact about Future _itself_. But, as I say, that's just a fact about Future; it isn't about the _sequence_ problem. You can prevent the Future from being executed on creation by using Deferred, or you can make the thing inside `.append` be a call to a function that creates and returns the Future (as in my answer here: https://stackoverflow.com/a/60418000/341994), or use `.flatMap`, etc. But the OP's question was not, "Hey, when I simply define my Future it starts running immediately! How do I prevent that?" The OP's question was about _serializing_ publishers. – matt Mar 09 '20 at 01:54
  • Using a function to create the Futures won't help if you are using append because all the fuctions that create the futures will be called when the sequence is initialized (flatMap would work though.) And the OP's question was "Is there a Combine operator that will _run_ a series of futures in sequence?" The question was _exactly_ about running the futures in sequence instead of them all running immediately. – Daniel T. Mar 09 '20 at 14:09