Home > OS >  How to apply compactMap to two combined publishers
How to apply compactMap to two combined publishers

Time:09-06

Assume the two following publishers:

var firstFeed = PassthroughSubject<Int?, Never>()
var secondFeed = PassthroughSubject<Int?, Never>()

How do I create a subscription where the sink is called when the two feed provide two none-nil values. This is what I have so far, but it seems ugly.

   let sub = firstFeed
        .compactMap{$0}
        .combineLatest(secondFeed.compactMap({$0}))
        .sink { firstValue, secondValue in
            print("we sunk with \(firstValue), \(secondValue)")
        }

With the following stream of data:

    firstFeed.send(1)
    secondFeed.send(nil)
    secondFeed.send(2)
    firstFeed.send(nil)
    firstFeed.send(3)
    firstFeed.send(nil)
    secondFeed.send(nil)
    firstFeed.send(nil)
    secondFeed.send(nil)
    secondFeed.send(4)
    firstFeed.send(nil)
    secondFeed.send(nil)
    firstFeed.send(5)
    secondFeed.send(6)
    secondFeed.send(nil)
    firstFeed.send(nil)
    firstFeed.send(7)
    firstFeed.send(nil)
    secondFeed.send(8)

I get this output:

we sunk with 1, 2
we sunk with 3, 2
we sunk with 3, 4
we sunk with 5, 4
we sunk with 5, 6
we sunk with 7, 6
we sunk with 7, 8

ideally I would want something like this:

    let sub = firstFeed
        .combineLatest(secondFeed)
        .compactMap{?}
        .sink { firstValue, secondValue in
            print("we sunk with \(firstValue), \(secondValue)")
        }

I'm not sure what goes into the compact map when two publishers are combined...

CodePudding user response:

To be honest, I don't really think your first way is "ugly". I would just use it as it is.

If you really want to use compactMap on a publisher of pairs, well... compactMap wants you to transform the given element to nil to mean that you want this element to be removed. For mapping T? to T, this is easy, because the things we want to remove are exactly nil!

However, you want to map from (Int?, Int?) to (Int, Int), so we need to produce a nil value of (Int, Int)? when we find either of the values in the pair is nil, and return the pair, with its elements unwrapped, otherwise.

In short, you are trying to convert from a (Int?, Int?) to a (Int, Int)?

.compactMap { x, y -> (Int, Int)? in
    guard let first = x, let second = y else { return nil }
    return (first, second)
}

You can also do it in just one expression:

.compactMap { first, second in
    first.flatMap { x in second.flatMap { y in (x, y) } }
}

There is no built in way to conveniently turn (Int?, Int?) into (Int, Int)?, but I have found this proposal that suggest this conversion should be a built in function.

You can write such a function yourself to make this look a bit prettier:

func unwrap<A, B>(pair: (A?, B?)) -> (A, B)? {
    pair.0.flatMap { a -> (A, B)? in
        pair.1.flatMap { b -> (A, B)? in
            (a, b)
        }
    }
}

let sub = firstFeed
    .combineLatest(secondFeed)
    .compactMap(unwrap)
    .sink { firstValue, secondValue in
        print("we sunk with \(firstValue), \(secondValue)")
    }

CodePudding user response:

You can use filter and map to skip nil values:

let sub = firstFeed
    .combineLatest(secondFeed)
    .filter { $0.0 != nil && $0.1 != nil }
    .map { ($0.0!, $0.1!) }
    .sink { firstValue, secondValue in
        print("we sunk with \(firstValue), \(secondValue)")
    }

, or compactMap to combine the filter and map into one, but maybe less readable, condition:

let sub = firstFeed
    .combineLatest(secondFeed)
    .compactMap { $0.0 != nil && $0.1 != nil ? ($0.0!, $0.1!) : nil }
    .sink { firstValue, secondValue in
        print("we sunk with \(firstValue), \(secondValue)")
    }
  • Related