Scala 3 — Book

Collections Methods

Language

A great strength of Scala collections is that they come with dozens of methods out of the box, and those methods are consistently available across the immutable and mutable collections types. The benefits of this are that you no longer need to write custom for loops every time you need to work with a collection, and when you move from one project to another, you’ll find these same methods used, rather than more custom for loops.

There are dozens of methods available to you, so they aren’t all shown here. Instead, only some of the most commonly-used methods are shown, including:

  • map
  • filter
  • foreach
  • head
  • tail
  • take, takeWhile
  • drop, dropWhile
  • reduce

The following methods work on all of the sequence types, including List, Vector, ArrayBuffer, etc., but these examples use a List unless otherwise specified.

As a very important note, none of the methods on List mutate the list. They all work in a functional style, meaning that they return a new collection with the modified results.

Examples of common methods

To give you an overview of what you’ll see in the following sections, these examples show some of the most commonly used collections methods. First, here are some methods that don’t use lambdas:

val a = List(10, 20, 30, 40, 10)      // List(10, 20, 30, 40, 10)

a.distinct                            // List(10, 20, 30, 40)
a.drop(2)                             // List(30, 40, 10)
a.dropRight(2)                        // List(10, 20, 30)
a.head                                // 10
a.headOption                          // Some(10)
a.init                                // List(10, 20, 30, 40)
a.intersect(List(19,20,21))           // List(20)
a.last                                // 10
a.lastOption                          // Some(10)
a.slice(2,4)                          // List(30, 40)
a.tail                                // List(20, 30, 40, 10)
a.take(3)                             // List(10, 20, 30)
a.takeRight(2)                        // List(40, 10)

Higher-order functions and lambdas

Next, we’ll show some commonly used higher-order functions (HOFs) that accept lambdas (anonymous functions). To get started, here are several variations of the lambda syntax, starting with the longest form, working in steps towards the most concise form:

// these functions are all equivalent and return
// the same data: List(10, 20, 10)

a.filter((i: Int) => i < 25)   // 1. most explicit form
a.filter((i) => i < 25)        // 2. `Int` is not required
a.filter(i => i < 25)          // 3. the parens are not required
a.filter(_ < 25)               // 4. `i` is not required

In those numbered examples:

  1. The first example shows the longest form. This much verbosity is rarely required, and only needed in the most complex usages.
  2. The compiler knows that a contains Int, so it’s not necessary to restate that here.
  3. Parentheses aren’t needed when you have only one parameter, such as i.
  4. When you have a single parameter, and it appears only once in your anonymous function, you can replace the parameter with _.

The Anonymous Function provides more details and examples of the rules related to shortening lambda expressions.

Now that you’ve seen the concise form, here are examples of other HOFs that use the short-form lambda syntax:

a.dropWhile(_ < 25)   // List(30, 40, 10)
a.filter(_ > 100)     // List()
a.filterNot(_ < 25)   // List(30, 40)
a.find(_ > 20)        // Some(30)
a.takeWhile(_ < 30)   // List(10, 20)

It’s important to note that HOFs also accept methods and functions as parameters—not just lambda expressions. Here are some examples of the map HOF that uses a method named double. Several variations of the lambda syntax are shown again:

def double(i: Int) = i * 2

// these all return `List(20, 40, 60, 80, 20)`
a.map(i => double(i))
a.map(double(_))
a.map(double)

In the last example, when an anonymous function consists of one function call that takes a single argument, you don’t have to name the argument, so even _ isn’t required.

Finally, you can combine HOFs as desired to solve problems:

// yields `List(100, 200)`
a.filter(_ < 40)
 .takeWhile(_ < 30)
 .map(_ * 10)

Sample data

The examples in the following sections use these lists:

val oneToTen = (1 to 10).toList
val names = List("adam", "brandy", "chris", "david")

map

The map method steps through each element in the existing list, applying the function you supply to each element, one at a time; it then returns a new list with all of the modified elements.

Here’s an example of the map method being applied to the oneToTen list:

scala> val doubles = oneToTen.map(_ * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

You can also write anonymous functions using a long form, like this:

scala> val doubles = oneToTen.map(i => i * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

However, in this lesson we’ll always use the first, shorter form.

Here are a few more examples of the map method being applied to the oneToTen and names lists:

scala> val capNames = names.map(_.capitalize)
capNames: List[String] = List(Adam, Brandy, Chris, David)

scala> val nameLengthsMap = names.map(s => (s, s.length)).toMap
nameLengthsMap: Map[String, Int] = Map(adam -> 4, brandy -> 6, chris -> 5, david -> 5)

scala> val isLessThanFive = oneToTen.map(_ < 5)
isLessThanFive: List[Boolean] = List(true, true, true, true, false, false, false, false, false, false)

As shown in the last two examples, it’s perfectly legal (and common) to use map to return a collection that has a different type than the original type.

filter

The filter method creates a new list containing the element that satisfy the provided predicate. A predicate, or condition, is a function that returns a Boolean (true or false). Here are a few examples:

scala> val lessThanFive = oneToTen.filter(_ < 5)
lessThanFive: List[Int] = List(1, 2, 3, 4)

scala> val evens = oneToTen.filter(_ % 2 == 0)
evens: List[Int] = List(2, 4, 6, 8, 10)

scala> val shortNames = names.filter(_.length <= 4)
shortNames: List[String] = List(adam)

A great thing about the functional methods on collections is that you can chain them together to solve problems. For instance, this example shows how to chain filter and map:

oneToTen.filter(_ < 4).map(_ * 10)

The REPL shows the result:

scala> oneToTen.filter(_ < 4).map(_ * 10)
val res1: List[Int] = List(10, 20, 30)

foreach

The foreach method is used to loop over all elements in a collection. Note that foreach is used for side-effects, such as printing information. Here’s an example with the names list:

scala> names.foreach(println)
adam
brandy
chris
david

The head method comes from Lisp and other earlier functional programming languages. It’s used to access the first element (the head element) of a list:

oneToTen.head   // 1
names.head      // adam

Because a String can be seen as a sequence of characters, you can also treat it like a list. This is how head works on these strings:

"foo".head   // 'f'
"bar".head   // 'b'

head is a great method to work with, but as a word of caution it can also throw an exception when called on an empty collection:

val emptyList = List[Int]()   // emptyList: List[Int] = List()
emptyList.head                // java.util.NoSuchElementException: head of empty list

Because of this you may want to use headOption instead of head, especially when programming in a functional style:

emptyList.headOption          // None

As shown, it doesn’t throw an exception, it simply returns the type Option that has the value None. You can learn more about this programming style in the Functional Programming chapter.

tail

The tail method also comes from Lisp, and it’s used to print every element in a list after the head element. A few examples demonstrate this:

oneToTen.head   // 1
oneToTen.tail   // List(2, 3, 4, 5, 6, 7, 8, 9, 10)

names.head      // adam
names.tail      // List(brandy, chris, david)

Just like head, tail also works on strings:

"foo".tail   // "oo"
"bar".tail   // "ar"

tail throws a java.lang.UnsupportedOperationException if the list is empty, so just like head and headOption, there’s also a tailOption method, which is preferred in functional programming.

A list can also be matched, so you can write expressions like this:

val x :: xs = names

Putting that code in the REPL shows that x is assigned to the head of the list, and xs is assigned to the tail:

scala> val x :: xs = names
val x: String = adam
val xs: List[String] = List(brandy, chris, david)

Pattern matching like this is useful in many situations, such as writing a sum method using recursion:

def sum(list: List[Int]): Int = list match {
  case Nil => 0
  case x :: xs => x + sum(xs)
}
def sum(list: List[Int]): Int = list match
  case Nil => 0
  case x :: xs => x + sum(xs)

take, takeRight, takeWhile

The take, takeRight, and takeWhile methods give you a nice way of “taking” the elements from a list that you want to use to create a new list. This is take and takeRight:

oneToTen.take(1)        // List(1)
oneToTen.take(2)        // List(1, 2)

oneToTen.takeRight(1)   // List(10)
oneToTen.takeRight(2)   // List(9, 10)

Notice how these methods work with “edge” cases, where we ask for more elements than are in the sequence, or ask for zero elements:

oneToTen.take(Int.MaxValue)        // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.takeRight(Int.MaxValue)   // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.take(0)                   // List()
oneToTen.takeRight(0)              // List()

And this is takeWhile, which works with a predicate function:

oneToTen.takeWhile(_ < 5)       // List(1, 2, 3, 4)
names.takeWhile(_.length < 5)   // List(adam)

drop, dropRight, dropWhile

drop, dropRight, and dropWhile are essentially the opposite of their “take” counterparts, dropping elements from a list. Here are some examples:

oneToTen.drop(1)        // List(2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.drop(5)        // List(6, 7, 8, 9, 10)

oneToTen.dropRight(8)   // List(1, 2)
oneToTen.dropRight(7)   // List(1, 2, 3)

Again notice how these methods work with edge cases:

oneToTen.drop(Int.MaxValue)        // List()
oneToTen.dropRight(Int.MaxValue)   // List()
oneToTen.drop(0)                   // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.dropRight(0)              // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

And this is dropWhile, which works with a predicate function:

oneToTen.dropWhile(_ < 5)       // List(5, 6, 7, 8, 9, 10)
names.dropWhile(_ != "chris")   // List(chris, david)

reduce

When you hear the term, “map reduce,” the “reduce” part refers to methods like reduce. It takes a function (or anonymous function) and applies that function to successive elements in the list.

The best way to explain reduce is to create a little helper method you can pass into it. For example, this is an add method that adds two integers together, and also provides us some nice debug output:

def add(x: Int, y: Int): Int = {
  val theSum = x + y
  println(s"received $x and $y, their sum is $theSum")
  theSum
}
def add(x: Int, y: Int): Int =
  val theSum = x + y
  println(s"received $x and $y, their sum is $theSum")
  theSum

Given that method and this list:

val a = List(1,2,3,4)

this is what happens when you pass the add method into reduce:

scala> a.reduce(add)
received 1 and 2, their sum is 3
received 3 and 3, their sum is 6
received 6 and 4, their sum is 10
res0: Int = 10

As that result shows, reduce uses add to reduce the list a into a single value, in this case, the sum of the integers in the list.

Once you get used to reduce, you’ll write a “sum” algorithm like this:

scala> a.reduce(_ + _)
res0: Int = 10

Similarly, a “product” algorithm looks like this:

scala> a.reduce(_ * _)
res1: Int = 24

An important concept to know about reduce is that—as its name implies—it’s used to reduce a collection down to a single value.

Even more

There are literally dozens of additional methods on the Scala collections types that will keep you from ever needing to write another for loop. See Mutable and Immutable Collections and The Architecture of Scala Collections for many more details on the Scala collections.

As a final note, if you’re using Java code in a Scala project, you can convert Java collections to Scala collections. By doing this you can use those collections in for expressions, and can also take advantage of Scala’s functional collections methods. See the Interacting with Java section for more details.

Contributors to this page: