1. Introduction
Scala provides very powerful Collection data structures in the standard library. The Scala collections library provides various methods/functions to process and manipulate data in our collection.
In this tutorial, we’ll explore different techniques to filter elements from a collection. We’ll focus on the find, filter, and collect methods and their respective variations.
Collections in Scala are immutable by default. Therefore, the methods we’ll see return new collections after applying the transformation to the input. In other words, these methods won’t modify the existing collections.
Unless specifically mentioned, these methods can be applied to all collection types, such as List, Array, and so on.
2. find Method
The find method on a Scala collection returns the first element matching the predicate as an Option value. If there is no matching element, it returns None.
Let’s look at a simple example to understand this:
val numbers = List(1, 2, 3, 4, 5, 6)
numbers.find(_ % 2 == 0) shouldBe Some(2)
numbers.find(_ > 9) shouldBe None
The find method utilizes the underlying collection’s ordering to find the first match.
In linear collections such as List, Seq, Array, and so on, we can use the method findLast to find the last element that matches the predicate:
val numbers = List(1, 2, 3, 4, 5, 6)
numbers.findLast(_ % 2 == 0) shouldBe Some(6)
However, we should note that this isn’t available on non-linear collections such as Set.
3. filter Method
We can use the filter method to select a set of elements from a collection matching a predicate.
Let’s look at a simple example:
val numbers: List[Int] = List(1, 2, 3, 4, 5, 6)
val oddNumbers: List[Int] = numbers.filter(_ % 2 == 1)
oddNumbers shouldBe List(1, 3, 5)
If none of the elements match the predicate, it returns an empty collection of the same type:
val numbers = List(2, 4, 6, 8)
val oddNumbers = numbers.filter(_ % 2 == 1)
oddNumbers shouldBe empty
We can also provide a complex predicate in the filter method:
val numbers = List(1, 2, 3, 4, 5, 6)
val oddAndNotMulOf3or5 =
numbers.filter(n => (n % 2 == 1 && n % 3 != 0 && n % 5 != 0))
oddAndNotMulOf3or5 shouldBe List(1)
Moreover, we can extract the complex condition into a method and then pass it on to the filter method. Let’s create a method corresponding to the complex predicate:
def isOddAndNotMulOf3or5(n: Int) =
n % 2 == 1 && n % 3 != 0 && n % 5 != 0
Now, we can apply this method to the filter method:
val numbers = List(1, 2, 3, 4, 5, 6)
val oddAndNotMulOf3or5 =
numbers.filter(isOddAndNotMulOf3or5)
oddAndNotMulOf3or5 shouldBe List(1)
Under the hood, Scala converts the method isOddAndNotMulOf3or5 into a function using eta expansion and applies the predicate to the filter method.
Scala offers an alternate method filterNot that inverts the condition. The filterNot method returns elements that don’t match the predicate condition:
val numbers = List(1, 2, 3, 4, 5, 6)
val nonOdd = numbers.filterNot(_ % 2 == 1)
val even = numbers.filter(_ % 2 != 1)
nonOdd shouldBe even
Here, both nonOdd and even variables should have the same values.
4. collect Method
We can also choose elements that meet a specific condition by using the collect method. Instead of a predicate function like with the filter method, collect uses a partial function.
Let’s rewrite the previous filter example using the collect method:
val numbers = List(1, 2, 3, 4, 5, 6)
val even = numbers.collect {
case n if n % 2 == 0 => n
}
even shouldBe List(2, 4, 6)
This looks more complicated and verbose than using a filter method. Nonetheless, the advantage of using the collect method lies in its capability to filter elements based on a given condition and transform the matching values. That means it combines the power of filter and map into a single method.
For example, let’s assume that we need to find the square of the even numbers:
val numbers = List(1, 2, 3, 4, 5, 6)
val even = numbers.collect {
case n if n % 2 == 0 => n * n
}
even shouldBe List(4, 16, 36)
This is comparable to the below code that uses filter and map:
val numbers = List(1, 2, 3, 4, 5, 6)
val evenSquared = numbers.filter(_ % 2 == 0).map(n => n * n)
evenSquared shouldBe List(4, 16, 36)
Additionally, we can use the method collectFirst to process only the first matching element:
val numbers = List(1, 2, 3, 4, 5, 6)
val firstEven = numbers.collectFirst {
case n if n % 2 == 0 => n
}
firstEven shouldBe Some(2)
The collect method can be likened to filter, while collectFirst can be likened to find. However, there is no method with the name collectLast. One of the ways to implement this is by reversing the collection and applying collectFirst on it.
5. Conclusion
In this article, we discussed the most common methods to filter elements from a collection. The methods find, filter, and collect can be used based on the requirement. We also looked at some variations of these methods.
The methods we discussed are the most popular ones for filtering elements. It’s worth noting that more methods, such as span, partition, and so on, can also be used to do some filtering.
As always, the sample code used in this tutorial is available over on GitHub.