Combine's filter and compactMap operators share a few similarities and it is possible to use them interchangeably in some scenarios. That said, there are a number of key differences we discuss in today's episode of Combine Essentials.

Similarities Between Filter and CompactMap

It is possible to use Combine's filter and compactMap operators interchangeably in some scenarios. Let's take a look at an example.

The reachablePublisher publisher emits true if the device has a network connection and false if the device isn't connected to the network. By applying the filter operator, the application only fetches a list of episodes from a remote API if the device is reachable.

private func setupBindings() {
    reachablePublisher
        .filter { $0 }
        .sink { [weak self] _ in
            self?.fetchEpisodes()
        }.store(in: &subscriptions)
}

The filter operator accepts a closure that returns true or false. If the closure returns true for an element, the filter operator republishes the element. If the closure returns false for an element, the filter operator doesn't republish the element. That is the filter operator in a nutshell.

The behavior of the compactMap operator is similar in some ways. Let's update the example by replacing the filter operator with the compactMap operator. The resulting behavior is identical.

private func setupBindings() {
    reachablePublisher
        .compactMap { $0 ? $0 : nil }
        .sink { [weak self] _ in
            self?.fetchEpisodes()
        }.store(in: &subscriptions)
}

The compactMap operator accepts a closure just like the filter operator. The difference is that the return value of the closure is T?, a generic, optional type. The compactMap operator defines what type it returns.

The compactMap operator can be used to filter the elements of its upstream publisher because it excludes elements for which its closure returns nil. What does that mean? Let's analyze the closure that is passed to the compactMap operator. Let's make the implementation easier to understand by avoiding shorthand argument syntax.

private func setupBindings() {
    reachablePublisher
        .compactMap { isReachable in
            isReachable
                ? isReachable
                : nil
        }
        .sink { [weak self] _ in
            self?.fetchEpisodes()
        }.store(in: &subscriptions)
}

If isReachable is equal to true, the closure returns the value of isReachable. If isReachable is equal to false, the closure returns nil. If the closure that is passed to the compactMap operator returns nil for an element, then that element isn't republished by the compactMap operator. That is how the compactMap operator can be used, and often is used, to filter the elements of an upstream publisher.

More Powerful Filtering

You could see the compactMap operator as a combination of the map and filter operators, but even that comparison isn't accurate. Let's take a look at the example we used in the previous episode of Combine Essentials. We declare a private, variable property, currentSelection, of type Int? and apply the Published property wrapper to it.

@Published private var currentSelection: Int?

We define an enum, TabBarItem, that defines a case for each tab bar item of the application's tab bar. The raw values for TabBarItem are defined to be of type Int. We want to transform the elements emitted by the publisher of currentSelection to TabBarItem objects. We apply the filter operator to exclude nil values and apply the map operator to transform the elements to TabBarItem objects.

@Published private var currentSelection: Int?

var currentSelectionPublisher: AnyPublisher<TabBarItem?, Never> {
    $currentSelection
        .filter { $0 != nil }
        .map { rawValue in
            guard let rawValue = rawValue else {
                return nil
            }

            return TabBarItem(rawValue: rawValue)
        }
        .eraseToAnyPublisher()
}

Notice that the Output type of currentSelectionPublisher is TabBarItem?, an optional type. This is inevitable since the filter operator doesn't change the Output type of the upstream publisher and the map operator doesn't exclude nil values.

The compactMap operator is different. It can change the Output type of the upstream publisher and it excludes nil values. Take a look at the updated example in which we replace the filter and map operators with the compactMap operator.

@Published private var currentSelection: Int?

var currentSelectionPublisher: AnyPublisher<TabBarItem, Never> {
    $currentSelection
        .compactMap { rawValue in
            guard let rawValue = rawValue else {
                return nil
            }

            return TabBarItem(rawValue: rawValue)
        }
        .eraseToAnyPublisher()
}

The changes are small. We removed the filter operator and replaced the map operator with the compactMap operator. Notice that the Output type of currentSelectionPublisher is TabBarItem, a non-optional type.

When to Use the Map Operator

You may think that the compactMap operator is preferred over the map operator, but that isn't true. First, if the Output type of the upstream publisher isn't an optional type, then there is no need to apply the compactMap operator. The map operator suffices in that scenario. Second, excluding nil values isn't always what you want or need. nil defines the absence of a value and that is sometimes desired. Take a look at this example.

@Published private var profile: Profile?

var currentProfilePublisher: AnyPublisher<Profile?, Never> {
    $profile
        .eraseToAnyPublisher()
}

The Output type of currentProfilePublisher is Profile?, an optional type. If currentProfilePublisher emits nil, it indicates the absence of a profile. This could mean the user isn't signed in. That is important information. Whether it makes sense to apply the map operator or the compactMap operator is often defined by the context in which they are used.

What's Next?

Even though Combine's filter and compactMap operators share a few similarities, they each have their use. Remember that the filter operator doesn't change the Output type of the upstream publisher. This isn't true for the compactMap operator. It can act like the filter operator, but it has the ability to transform the elements of the upstream publisher and change the optionality of the upstream publisher's Output type.