Mastering Scala Basics: Collections

Scala collections systematically distinguish between mutable (scala.collection.mutable) and immutable (scala.collection.immutable) collections. A mutable collection can be updated or extended in place. This means you can change, add, or remove elements of a collection as a side effect. Immutable collections, by contrast, never change. You have still operations that simulate additions, removals, or updates, but those operations will in each case return a new collection and leave the old collection unchanged.

The following figure shows all collections in package scala.collection

Collections in Scala
Collections in Scala

A sequence is a collection of items with a defined and stable order.

// create a sequence
val sequence = Seq(1,2,3,4)
// val sequence: Seq[Int] = List(1, 2, 3, 4)
// the value has type Seq[Int] but implementation is List. 
// This is the key feature of Scala's collections, the separation between
// interface and implementations

// find value at index 1
sequence(1)
// val res0: Int = 2

// get tail of the sequence
sequence.tail
// val res1: Seq[Int] = List(2, 3, 4)

// length of the sequence
sequence.length
// val res2: Int = 4

// check if the sequence contains an element
sequence.contains(1)
// val res3: Boolean = true

// similar to contains, if contains then return the first element
sequence.find(_ > 2)
// val res4: Option[Int] = Some(3)

// add value to the sequence
sequence :+ 10
// val res5: Seq[Int] = List(1, 2, 3, 4, 10)

The default implementation of Seq is List.

// create empty list using the singleton object Nil
Nil
val res6: collection.immutable.Nil.type = List()

// prepending elements
val list = 1 :: 3 :: 5 :: Nil
// val list: List[Int] = List(1, 3, 5)

10 :: list
// val res7: List[Int] = List(10, 1, 3, 5)

// creating list using apply method
List(1,2,3)
// val res8: List[Int] = List(1, 2, 3)

Working with Sequences

Map - map takes a function and applies it to every element, creating a sequence of the results.

sequence.map(_ * 2)
// val res9: Seq[Int] = List(2, 4, 6, 8)

sequence.map(e => e * 2)
// val res10: Seq[Int] = List(2, 4, 6, 8)

Given a sequence with type Seq[A], the function we must pass to map must have type A ⇒ B and we get a Seq[B] as a result.

FlatMap - flatMap is used to collect a single flat sequence.

Seq(1,2,3).map(e => Seq(e, e*10))
// val res11: Seq[Seq[Int]] = List(List(1, 10), List(2, 20), List(3, 30))

// If we want just a list, use flatMap
Seq(1,2,3).flatMap(e => Seq(e, e*10))
// val res12: Seq[Int] = List(1, 10, 2, 20, 3, 30)

Fold - When we want to return a single element, use Fold. For example: sum of the elements in a list

// evaluation starts on the left
// (((0 +1)+2) +3)
Seq(1,2,3).foldLeft(0)(_+_)
// val res15: Int = 6

// evaluation starts on the right
// (1+ (2+(3+ 0)))
Seq(1,2,3).foldRight(0)(_+_)
// val res17: Int = 6

Foreach - foreach is purely used for side effects.

List(1,2,3).foreach(n => println("Number is: "+ n))
// Number is: 1
// Number is: 2
// Number is: 3
We have We provide We want Method
Seq[A] A ⇒ Unit Unit foreach
Seq[A] A ⇒ B Seq[B] map
Seq[A] A ⇒ Seq[B] Seq[B] flatMap
Seq[A] B and (B, A) ⇒ B B foldLeft
Seq[A] B and (A, B). ⇒ B B foldRight

For Comprehensions - for comprehensions make complicated operations simple to write.

Seq(1,2,3).map(_ * 2)
// val res18: Seq[Int] = List(2, 4, 6)

for {
    x <- Seq(1,2,3)
} yield x*2
// val res19: Seq[Int] = List(2, 4, 6)

<- is a generation, with a pattern on the left and a generator expression on the right.A for comprehension iterates over the elements in the generator, binding each element to the pattern and calling the yield expression. It combines the yielded results into a sequence of the same type as the original generator.

Let’s look at another example (see the simplicity both in writing and reading with for comprehension):

val data = Seq(Seq(10), Seq(20, 30), Seq(2, 4))
// val data: Seq[Seq[Int]] = List(List(10), List(20, 30), List(2, 4))

data.flatMap(_.map(_ / 2))
// val res20: Seq[Int] = List(5, 10, 15, 1, 2)

for {
    d <- data
    e <- d
} yield e / 2
// val res21: Seq[Int] = List(5, 10, 15, 1, 2)

A general rule for for comprehension:

for {
    x <- a
    y <- b
    z <- c 
} yield e

// translates to

a.flatMap(x => b.flatMap(y => c.map(z => e)))

Few more examples of for comprehension:

for(n <- Seq(10,20,30,50) if n > 20) yield n
// val res23: Seq[Int] = List(30, 50)

for {
    x <- Seq(1,3,5)
    y <- Seq(2,4,6)
} yield x + y
// val res24: Seq[Int] = List(3, 5, 7, 5, 7, 9, 7, 9, 11)

for {
    x <- Seq(1,2,3)
    square = x * x
    y <- Seq(4,5,6)
} yield square * y
// val res25: Seq[Int] = List(4, 5, 6, 16, 20, 24, 36, 45, 54)

Look into Scala Collection if you want to learn more about it.

Option

Option in Scala represents optional values. Instances of Option can either be Some[T] or None, which represents a missing value. Option is just list data structure that stores a data, the reason why we use Option is when there’s a chance of absence of a value.

val anOption: Option[Int] = Some(10)
// val anOption: Option[Int] = Some(10)

val anEmptyOption: Option[Int] = None
// val anEmptyOption: Option[Int] = None

It is mostly used for APIs that can return nulls, wrap the type of those APIs in Options when you use them.

References used are Scala Doc and Essential Scala