How can I filter a tuple with a Combine operator?

Hi all!

Given this Publisher-tuple

let tuple = (1, [1, 2, 3])

How can I filter the number 3 using a Combine Operator?

Thanks :)

Your question needs a bit more detail in order to be able to help you. We're talking about a stream of inputs, and a stream of outputs - where those types are all the same. So is what you're asking:

  • filter all the (Int,[Int]) types that I provide down to ones where the internal [int] array contains a 3
  • or are you trying to get something else out of the output, transforming it in some way, such as removing any 3 from the [int] array?

In either case, my first thought for this kind of scenario is to use the Filter operator, but depending on how you want to deal with the results, it might be some combination of filter, map, and/or compactMap. If you're trying to "remove the 3's" then some version of map - transforming the values - makes more sense.

4 Likes

I will use a graphic to explain myself better. Thanks

So definitely .map in some fashion, and you're wanting to transform the input type into some other output type - it looks like (Int,[Int]) -> Int from your notes.

But it's still not clear how you want to get the to 3 and what that means. Are you trying to get to the 3rd index of whatever is in that array, or do you want to filter that array for a specific value (3), or do you want just the "last value" of the array?

If you're wanting to filter for a specific value - how do you want to handle the situation where there could be identical values in the array? So if your array has [1,2,3,3,4,5,3], should the result be a single 3 or an array of [3,3,3] (which also implies the output type shouldn't be Int, but [Int]. Likewise for a filtering option what output do you want if there's NO 3 value in the array?

The questions I'm asking are partially about what you need, but also tuned specifically to functional mechanisms that you might use, which sort of identifies what kinds of operators you want to use.

Another way to think about how to solve this is to think of the problem as a sort of puzzle, where you can either transform a value or filter out specific values, and the puzzle is coming up with the series of steps that takes the input you're given and comes out with the right result. (those two puzzle samples are the classic examples of "map" and "reduce" in functional programming, if you're so inclined to look up that kind of detail). If you can identify the steps, then you can pretty quickly identify the relevant operators to hook up in a series to achieve what you're after.

1 Like

It's not clear how you decided that 3 is the particular value you're interested in. If you always want the last element of the array, you can do it like this:

import Combine

let subject = PassthroughSubject<(Int, [Int]), Never>()

let ticket = subject
    .map { $0.1.last }
    .sink {
        if let i = $0 {
            print("got \(i)")
        } else {
            print("got nil")
        }
    }

subject.send((1, [1, 2, 3]))
// output: got 3
1 Like

First, thank you very much @Joseph_Heck and @mayoff for your replies.

Sorry for the delay and the incomplete information in my previous posts.
I will try to be more clear.

In my app, I am using the CoreBluetooth framework to connect to a specific peripheral.

The steps are the following:

I connect with the desired Peripheral
I filter (of all the Services offered by the Peripheral) the specific Service I want to use
Finally, within that Service, I filter for the specific Characteristic I want to use (a Service can offer more than one Characteristic)

Up to this point, I don't use Combine.

The object I get is of this type:
(CBService, [CBCharacteristic])

Where the 1st item of the tuple represents the Service in question
And the 2nd item represents all the Characteristics associated with that service
In the specific case of my project, I know that I will always obtain 3 Characteristics associated with the Service that I am going to use

So what I get is:
(SERVICE, [CHARACTERISTIC_1, CHARACTERISTIC_2, CHARACTERISTIC_3])

From now on I use Combine to send this information via the .send method to this Subject:

var characteristicsSubject: PassthroughSubject<(CBService, [CBCharacteristic]), Never>

Once the var characteristicsSubject is populated, my intention is that ONLY the desired Characteristic reaches the Subscriber.

So, the starting point of the Pipeline is:

SUBJECT
<(CBService, [CBCharacteristic]), Never>

OPERATORS
Intermediate operations to do this can be: (if I'm not doing something wrong)

  1. I get the 2nd element of the tuple (the Characteristic array)
  2. I get from this array the desired Characteristic (using its UUID)

SUBSCRIBER
And the final result should be:
<CBCharacteristic, Never>

So think about this as the following steps:

  1. convert the input (CBService, [CBCharacteristic]) into [CBCharacteristic]
  2. filter [CBCharacteristic] to the one kind of characteristic that you're interested in

That pretty directly translates to using a .map() operator to do the conversion, then .filter() to select the characteristic. filter by itself wouldn't transform the type from [Int] to Int - it instead returns the same time, but with the values filtered. If you are absolutely sure that the values won't be duplicate, then you could choose to use the function first, which returns the first value that matches, but also changes the type of the value to an optional.

Note: the use of .first() inside the combine .map() operator is using the the first method from array, not Combine. It's mixing the two together to get both the reduction of values and the type transformation.

So tweaking Rob's example a bit:

import Combine

let subject = PassthroughSubject<(Int, [Int]), Never>()

let ticket = subject
    .map { $0.1 } // converts from (Int, [Int]) to [Int]
    .map { $0.first { $0 == 3 } } // convert from [Int] to Int?, selecting the specific value you're interested in
    .sink {
        if let i = $0 {
            print("got \(i)")
        } else {
            print("got nil")
        }
    }

subject.send((1, [1, 2, 3]))
// output: got 3

UPDATE:

I wasn't familiar with the details of CoreBluetooth, so I went and briefly looked at CBService and CBCharacteristic. That immediately tossed up a bit of red-flag for me, because according to the documentation, both are Classes - not values.

Combine, in particular, operates better with value types as compared to reference types. In this scenario, you may be far better off digging out the data you want using a function that destructures the tuple and searches for the relevant characteristic before you send the resulting value to Combine for something else to receive through the publisher. Otherwise, the combine bits will work - but using combine for this filtering/destructuring will be far less effective because it's copying around a bunch of data that you basically just don't need, and doing so with every instance you pop through the pipeline with .send() on your publisher.

1 Like

Great, thanks for your explanation :slight_smile:

1 Like