Scala from the ground up

Lists and their methods

An ordered collection that never changes, and the methods that do the looping for you: map, filter, foldLeft, sum, sortBy, mkString, zip, take and drop.

Part 8 of 12

Intermediate16 min readBack to Courses

An ordered collection that stays put

List(5, 3, 8, 2) builds a list in the order you gave, and printing it shows the values in that same order, inside round brackets. Lists do not change: map, filter and sortBy each hand back a new list and leave the original untouched, so a chain of them can be read left to right without wondering what an earlier step did to the data.

The methods that answer with a single value are the ones you reach for first. sum adds the numbers and length counts the items, and mkString joins everything into one line. Give mkString a separator and the pieces are glued together; give it a wrapper as well and the whole row is dressed up, as in mkString("[", ", ", "]").

map, filter and foldLeft

map turns each item into something else: nums.map(n => n * 2) gives a list of the same length with every value doubled. filter keeps the items that pass a test, so nums.filter(n => n % 2 == 0) gives the even ones and drops the rest. Both take a function value and both return a list, which is why they chain.

foldLeft is the general one. It carries an accumulator through the list from left to right, starting from a value you name: nums.foldLeft(0)((acc, n) => acc + n) is a sum spelled out in full. Once that reads naturally, so does a fold that builds a sentence or adds up one field from every record.

Order, pairs and slices

sortBy orders by whatever the function returns, so words.sortBy(w => w.length) puts the shortest word first and leaves words of equal length in the order they came. zip pairs two lists position by position and gives a list of tuples: List("a", "b").zip(List(1, 2)) is List((a,1), (b,2)), and the tuple parts are read as _1 and _2.

take and drop cut a list at a position. take(2) keeps the first two items, drop(2) throws those away and keeps the rest, and together they split the list in one place. Every one of these methods says what it wants rather than how to loop, which is the reason Scala code is short without being cryptic.

Chains, and the type at each step

Each method in a chain is a separate step with its own result. map returns a list of the same length and keeps the order, filter returns a list no longer than the one it was given, and foldLeft returns a single value of whatever type the accumulator has. Reading a chain left to right means knowing the shape after each call, and the compiler checks that the next call is allowed.

Lists are built to be shared. A method such as filter or take returns a new list and leaves the old one intact, so the original name still refers to the original items. That is why a chain can be stored in a val and reused, and why passing a list to a function cannot let the function change it behind your back.

Some operations are cheaper than others. Taking the first item and walking the rest are natural for a linked list, while reading position 500 means stepping through the earlier items first. nums(500) on a short list throws an index error rather than returning a default, so use take when you only need a prefix and the list might be shorter than you expect.

Three habits to avoid: using map when you only want side effects (foreach says that better), asking for nums(10) on a short list and expecting 0, and sorting descending with sortBy(n => -n) on values that might be the smallest possible Int. For plain positive scores the minus is fine; for the general case use sortBy(n => n).reverse.

Try these next

  • Filter a list of words down to the ones longer than three letters, then map them to uppercase.
  • Build one sentence from a list with foldLeft, putting a space between words.
  • Zip two lists of the same length and print only the pairs whose second value is above five.

One list, six answers

val nums = List(5, 3, 8, 2)

println(nums)                    // List(5, 3, 8, 2)
println(nums.sum)                // 18
println(nums.map(n => n * 2))    // List(10, 6, 16, 4)
println(nums.filter(n => n % 2 == 0))  // List(8, 2)
println(nums.foldLeft(0)((a, n) => a + n))  // 18

val words = List("pear", "fig", "banana")
println(words.sortBy(w => w.length)) // by length
println(nums.mkString("-"))      // 5-3-8-2
println(nums.take(2))            // List(5, 3)
println(nums.drop(2))            // List(8, 2)

A basket of numbers

Run it and check each line against the list at the top. Then filter the numbers above 3 and print the result, and add a fifth number to the list before reading the sum again.

Output
Click "Run" to execute your code

Words, scores and a sentence

Run it and check the length of each result against the list at the top. Then add a fifth word, zip it with a score, and read the total again.

Output
Click "Run" to execute your code

nums.sum and nums.foldLeft(0)((a, n) => a + n) give the same answer, and the shorter one is better when it exists. Reach for the fold when the accumulator is a string, a map or a record of your own.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does List(5, 3, 8, 2).filter(n => n % 2 == 0) give?