ReactiveCocoa / ReactiveSwift

Streams of values over time

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

flatScan operator?

mfclarke opened this issue · comments

Something I've found useful lately, particularly for APIs that provide say the next page or next cursor location in the response, is a combined flatMap and scan operator.

It looks like this:

func flatScan<U>(_ strategy: FlattenStrategy, initialResult: U, nextPartialResult: @escaping (U, Value) -> SignalProducer<U, Error>) -> Signal<U, Error> {
  var accumulator = initialResult
  return self
    .map { (accumulator, $0) }
    .flatMap(strategy) { nextPartialResult($0, $1) }
    .on(value: { accumulator = $0 })
}

It allows you to provide the nextPartialResult calculation in the form of a SignalProducer, where the accumulator and self value emission can be used to build the SignalProducer.

This allows you to do things like paginated API requests without any external state or side effects.

Is this something you guys would be interested in having in the standard ReactiveSwift operators?

I see the value in such operator. But we'd need a more cautious thought on whether any strategy other than concat and latest is applicable.

For example, with merge, one could have multiple work derived from the accumulator at time t running. When any of them updates the accumulator at time t+1, it makes other outstanding work sort of stale, and in turn potentially causes silent incorrect accumulation.

That implementation doesn't look correct to me. I think you're going to send the wrong accumulator at times.

Are you aware of any existing operators like this in any Rx libraries? (If not, could you do some research on this?)

@andersio good point, I can definitely see that being a problem.

@mdiep yes, it's highly likely that this isn't totally correct! The code above works well enough for my specific use case, but hasn't been fleshed out and tested for general use yet.

I also couldn't find anything in Rx when I was searching for something like this.

Perhaps I generalised too quickly on this one? It works great for single network requests per scan. I added in a flag to skip accumulator repeats, to avoid the closure being called before the accumulator has updated.

I'll bet there's a better way to do this, though it doesn't come to mind at the moment.

Just a thought: modifying the accumulator to be an array of values will help to alleviate some of these issues:

func flatScan<U>(_ strategy: FlattenStrategy, initialResult: U, nextPartialResult: @escaping ([U], Value) -> SignalProducer<U, Error>) -> Signal<U, Error> {
  var accumulator = [initialResult]
  
  return self
      .map { (accumulator, $0) }
      .flatMap(strategy) { nextPartialResult($0, $1) }
      .on(value: { accumulator += [$0] })
}

Then it's the responsibility of the caller to determine what values to keep, throw away, sum etc.

Hello. 👋 Thanks for opening this issue. Due to inactivity, we will soft close the issue. If you feel that it should remain open, please let us know. 😄