We can define a new List, Set, or Map, or, alternatively, change an existing one by adding or removing elements, like a football coach would manage the team players by adding or removing candidates for the match. For the sake of efficacy, we can perform other modifications, as well: substitute one player with another or filter players by strength.
In this topic, we will discuss the type of operations that can help us modify collections in Scala.
Copying with updates
In Scala, collections are typically immutable. So, to introduce any modifications, we should start with learning how to copy collections:
scala> val list = List(1, 2, 3)
list: List[Int] = List(1, 2, 3)
scala> list.updated(1, 5)
res1: List[Int] = List(1, 5, 3)
With the help of updated, we copied all the elements except the one specified by index and a new value for it. Note that we can't use updated with Set because it doesn't work with a defined index:
scala> val set = Set('a', 'b', 'c')
set: scala.collection.immutable.Set[Char] = Set(a, b, c)
scala> set.updated(1, 'c')
<console>:13: error: value updated is not a member of scala.collection.immutable.Set[Char]
set.updated(1, 'c')
Map allows for probably the most interesting updates. This follows from the nature of this data structure: if you match some value with a key, you will probably change the value at some point. You can do that easily with updated or +:
scala> val map = Map('a' -> 1, 'b' -> 2)
map: scala.collection.immutable.Map[Char,Int] = Map(a -> 1, b -> 2)
scala> map.updated('a', 0)
res1: scala.collection.immutable.Map[Char,Int] = Map(a -> 0, b -> 2)
scala> map + ('a' -> 1)
res2: scala.collection.immutable.Map[Char,Int] = Map(a -> 1, b -> 2)
scala> map + ('c' -> 3)
res3: scala.collection.immutable.Map[Char,Int] = Map(a -> 1, b -> 2, c -> 3)
Note that with updated in Maps, we use two arguments: the key and the new value, while with +, we just add a new pair as a single parameter. You can update and add new pairs with a single + operation.
Filtering
Scala has an interesting technique to transform one collection into another with the help of transforming methods. Imagine that you have a list of numbers and you want to get only the even numbers from it. For this purpose, you can use filter:
scala> val list = List(1, 2, 3, 4)
list: List[Int] = List(1, 2, 3, 4)
scala> def even: Int => Boolean = e => e % 2 == 0
even: Int => Boolean
scala> list.filter(even)
res1: List[Int] = List(2, 4)
Above, we iterated over the elements of the list and filtered only the even ones. To do that, we predefined a helper function even and passed it as a parameter. That function transformed the element to a Boolean value and if the result is true, it includes the element in the new collection. The same functionality could be applied to Set or Map:
scala> val set = Set(1, 2, 3)
set: scala.collection.immutable.Set[Int] = Set(1, 2, 3)
scala> def moreOne(i: Int) = i > 1
moreOne: (i: Int)Boolean
scala> set.filter(moreOne)
res1: scala.collection.immutable.Set[Int] = Set(2, 3)
scala> def isTupleAOne(tuple: (Char, Int)) = tuple._1 == 'a' && tuple._2 == 1
isTupleAOne: (tuple: (Char, Int))Boolean
scala> val map = Map('a' -> 0)
map: scala.collection.immutable.Map[Char,Int] = Map(a -> 0)
scala> map.filter(isTupleAOne)
res2: scala.collection.immutable.Map[Char,Int] = Map()
In the snippet above, we wanted to include in the new collection only the elements that can pass the filter. For Set, we specified a helper function to get only the numbers larger than one, and for Map, we want only the pairs (as elements are tuples) with 'a' as the first and "1" as the second.
Mapping
With filters, we can use a helper function to transform the element to Boolean. We can go further and ask: is it possible to define a helper function to transform an element to any other value and use it to make a new collection? This is exactly what the map operation does: we pass the function from one element to another and create a new collection with this technique:
scala> val list = List(1, 2, 3)
list: List[Int] = List(1, 2, 3)
scala> def plusOne(i: Int): Int = i + 1
plusOne: (i: Int)Int
scala> list.map(plusOne)
res1: List[Int] = List(2, 3, 4)
Above, we defined a function plusOne to increment the element value of the new collection. We can do the same for Set:
scala> val set = Set(1, 2, 3)
set: scala.collection.immutable.Set[Int] = Set(1, 2, 3)
scala> set.map(e => "elem" + e)
res1: scala.collection.immutable.Set[String] = Set(elem1, elem2, elem3)
Note that above, we used the inline form of the definition for the function, which is convenient if you need a helper only for one operation. In addition, the function transforms Int to String, so map gives you the ability to change the types of the collection's elements.
We can transform a Map as well:
scala> val map = Map('a' -> 1, 'b' -> 2)
map: scala.collection.immutable.Map[Char,Int] = Map(a -> 1, b -> 2)
scala> def cKey(pair: (Char, Int)) = 'c' -> pair._2
cKey: (pair: (Char, Int))(Int, Int)
scala> map.map(cKey)
res1: scala.collection.immutable.Map[Int,Int] = Map('c' -> 2)
Note that here, we mapped all the keys of Map to one value 'c', and the result contains only one pair as it is impossible to have several values with one key in this collection type.
Var and collections
It looks like we always have to make a new collection, but what if we want to change an existing one? For example, say we have a set of cinema tickets in our application, and these tickets are sold to users. If we generate a new collection without a ticket that has been sold, the original one still contains it and somebody could buy it again. To avoid this, you can init a collection as var and make changes in place:
scala> var tickets = Set("A1", "B4")
tickets: scala.collection.immutable.Set[String] = Set(A1, B4)
scala> tickets = tickets - "A1"
tickets: scala.collection.immutable.Set[String] = Set(B4)
scala> tickets = tickets - "B4"
tickets: scala.collection.immutable.Set[String] = Set()
We assigned the tickets variable to a new collection without the element that has been sold. We can use the same approach for saving variables we want to use in the future:
scala> var variables: Map[String,Int] = Map.empty
variables: Map[String,Int] = Map()
scala> variables += "n" -> 5
scala> variables
res1: Map[String,Int] = Map(n -> 5)
scala> variables("n")
res2: Int = 5
Note that we can use the operation += as a shortcut for a = a +.
Conclusion
Modification of collections gives you the ability to transform predefined ones in different ways. For example, updated substitutes an element with another one while copying others. Filtering passes only the elements that return true with a helper function to generate a collection. Mapping will use a helper function to transform elements to any other value and put them in a new collection. Finally, we can create a new collection as a 'variable' to make changes in place.