In JavaScript there are basically two kinds of collections you have used to store your data: the Array
for sequentialdata and Object
(aka dictionary or hash map) for storing key-value pairs. Furthermore both of these are mutable bydefault, so if you pass them to a function, that function might go and modify them without your knowledge.
ES6 extends your options with four new collection types Map
, Set
, WeakMap
andWeakSet
. Of these the WeakMap
and WeakSet
are for special purposes only, so in your application youwould typically use only Map
and Set
.
Scala collection hierarchy
Unlike JavaScript, the Scala standard library has a huge variety of different collection types to choose from.Furthermore the collections are organized in a type hierarchy, meaning they share a lot of common functionality andinterfaces. The high-level hierarchy for the abstract base classes and traits is shown in the image below.
Scala provides immutable and mutable implementations for all these collection types.
Common immutable collections | |
---|---|
Seq | List , Vector , Stream , Range |
Map | HashMap , TreeMap |
Set | HashSet , TreeSet |
Common mutable collections | |
Seq | Buffer , ListBuffer , Queue , Stack |
Map | HashMap , LinkedHashMap |
Set | HashSet |
Comparing to JavaScript
Let’s start with familiar things and see how Scala collections compare with the JavaScript Array
and Object
(orMap
). The closest match for Array
would be the mutable Buffer
since arrays in Scala cannot change size afterinitialization. For Object
(or Map
) the best match is the mutable HashMap
.
A simple example of array manipulation.
ES6
const a = ["Fox", "jumped", "over"];
a.push("me"); // Fox jumped over me
a.unshift("Red"); // Red Fox jumped over me
const fox = a[1];
a[a.length - 1] = "you"; // Red Fox jumped over you
console.log(a.join(" "));
Scala
import scala.collection.mutable
val a = mutable.Buffer("Fox", "jumped", "over")
a.append("me") // Fox jumped over me
a.prepend("Red") // Red Fox jumped over me
val fox = a(1)
a(a.length - 1) = "you" // Red Fox jumped over you
println(a.mkString(" "))
Working with a hash map (or Object).
ES6
const p = {first: "James", last: "Bond"};
p["profession"] = "Spy";
const name = `${p.first} ${p.last}`
Scala
import scala.collection.mutable
val p = mutable.HashMap("first" -> "James",
"last" -> "Bond")
p("profession") = "Spy"
val name = s"${p("first")} ${p("last")}"
Even though you can use Scala collections like you would use arrays and objects in JavaScript, you really shouldn’t,because you are missing a lot of great functionality.
Common collections Seq, Map, Set and Tuple
For 99% of the time you will be working with those four common collection types in your code. You will instantiateimplementation collections like Vector
or HashMap
, but in your code you don’t really care what the implementation is,as long as it behaves like a Seq
or a Map
.
Tuple
You may have noticed that Tuple
is not shown in the collection hierarchy above, because it’s a very specificcollection type of its own. Scala tuple combines a fixed number of items together so that they can be passed around as awhole. A tuple is immutable and can hold different types, so it’s quite close to an anonymous case class in that sense.Tuples are used in situations where you need to group items together, like key and value in a map, or to return multiplevalues. In JavaScript you can use a fixed size array to represent a tuple.
ES6
const t = ["James", "Bond", 42];
const kv = ["key", 42];
function sumProduct(s) {
let sum = 0;
let product = 1;
for(let i of s) {
sum += i;
product *= i;
}
return [sum, product];
}
Scala
val t = ("James", "Bond", 42)
val kv = "key" -> 42 // same as ("key", 42)
def sumProduct(s: Seq[Int]): (Int, Int) = {
var sum = 0
var product = 1
for(i <- s) {
sum += i
product *= i
}
(sum, product)
}
To access values inside a tuple, use the tuple.1
syntax, where the number indicates position within the tuple(starting from 1, not 0). Quite often you can also use _destructuring to extract the values.
ES6
const sc = sumProduct([1, 2, 3]);
const sum = sc[0];
const product = sc[1];
// with destructuring
const [sum, product] = sumProduct([1, 2, 3]);
Scala
val sc = sumProduct(Seq(1, 2, 3))
val sum = sc._1
val product = sc._2
// with destructuring
val (sum, product) = sumProduct(Seq(1, 2, 3))
Seq
Seq
is an ordered sequence. Typical implementations include List
, Vector
, Buffer
and Range
. Although ScalaArray
is not a Seq
, it can be wrapped into a WrappedArray
to enable all Seq
operations on arrays. In Scala this is done automatically through an implicit conversion, allowing you to write codelike following.
Scala
val ar = Array(1, 2, 3, 4)
val product = ar.foldLeft(1)((a, x) => a * x) // foldLeft comes from WrappedArray
The Seq
trait exposes many methods familiar to the users of JavaScript arrays, includingforeach
:Unit), map
:Seq[B]),filter
:Repr), slice
:Repr)and reverse
. In addition to these, there are several more useful methodsshown with examples in the code block below.
Scala
val seq = Seq(1, 2, 3, 4, 5)
seq.isEmpty == false
seq.contains(6) == false // JS Array.indexOf(6) == -1
seq.forall(x => x > 0) == true // JS Array.every()
seq.exists(x => x % 3 == 0) == true // JS Array.some()
seq.find(x => x > 3) == Some(4) // JS Array.find()
seq.head == 1
seq.tail == Seq(2, 3, 4, 5)
seq.last == 5
seq.init == Seq(1, 2, 3, 4)
seq.drop(2) == Seq(3, 4, 5)
seq.dropRight(2) == Seq(1, 2, 3)
seq.count(x => x < 3) == 2
seq.groupBy(x => x % 2) == Map(1 -> Seq(1, 3, 5), 0 -> Seq(2, 4))
seq.sortBy(x => -x) == Seq(5, 4, 3, 2, 1)
seq.partition(x => x > 3) == (Seq(4, 5), Seq(1, 2, 3))
seq :+ 6 == Seq(1, 2, 3, 4, 5, 6)
seq ++ Seq(6, 7) == Seq(1, 2, 3, 4, 5, 6, 7) // JS Array.concat()
The functionality offered by Array.reduce
in JavaScriptis covered by two distinct methods in Scala: reduceLeft
=>B):B)and foldLeft
(op:(B,A)=>B):B). The difference is that in foldLeft
you provide an initial (“zero”) value (which is an optional parameter to Array.reduce
) while in reduceLeft
you don’t.Also note that in foldLeft
, the type of the accumulator can be something else, for example a tuple, but in reduceLeft
it must always be a supertype of the value.Since reduceLeft
cannot deal with an empty collection, it is rarely useful.
ES6
function sumProduct(s) {
// destructuring works in the function argument
return s.reduce(([sum, product], x) =>
[sum + x, product * x],
[0, 1] // use an array to represent a tuple
);
}
Scala
def sumProduct(s: Seq[Int]): (Int, Int) = {
// use a tuple accumulator to hold sum and product
s.foldLeft((0, 1)) { case ((sum, product), x) =>
(sum + x, product * x)
}
}
Map
A Map
consists of pairs of keys and values. Both keys and values can be of any valid Scala type, unlike in JavaScriptwhere an Object
may only contain string
keys (the new ES6 Map
allows using other types as keys, but supports onlyreferential equality for comparing keys).
JavaScript Object
doesn’t really have methods for using it as a map, although you can iterate over the keyswith Object.keys
. When using Object
as a map, most developers use utility libraries likelodash to get access to suitable functionality. The ES6 Map
object containskeys
, values
and forEach
methods for accessing its contents, but alltransformation methods are missing.
You can build a map directly or from a sequence of key-value pairs.
ES6
// object style map
const m = {first: "James", last: "Bond"};
// ES6 Map
const data = [["first", "James"], ["last", "Bond"]];
const m2 = new Map(data);
Scala
val m = Map("first" -> "James", "last" -> "Bond")
val data = Seq("first" -> "James", "last" -> "Bond")
val m2 = Map(data:_*)
In Scala when a function expects a variable number of parameters (like the Map
constructor), you can destructure asequence with the seq:*
syntax, which is the equivalent of ES6’s _spread operator …seq
.
Accessing Map
contents can be done in many ways.
ES6
// object syntax
const name = `${m.last}, ${m.first} ${m.last}`
// ES6 Map syntax
const name2 = `${m2.get("last")}, ${m2.get("first")} ${m2.get("last")}`
// use default value when missing
const age = m.age === undefined ? "42" : m.age;
// check all fields are present
const person = m.first !== undefined &&
m.last !== undefined &&
m.age !== undefined ? `${m.last}, ${m.first}: ${m.age}` :
"missing";
Scala
val name = s"${m("last")}, ${m("first")} ${m("last")}"
// use default value when missing
val age = m.getOrElse("age", "42")
// check all fields are present
val person = (for {
first <- m.get("first")
last <- m.get("last")
age <- m.get("age")
} yield {
s"$last, $first: $age"
}).getOrElse("missing")
In the previous example m.get("first")
returns an Option[String]
indicating whether the key is present in the mapor not. By using a for comprehension, we can easily extract three separate values from the map and use them to build theresult. The result from for {} yield
is also an Option[String]
so we can use getOrElse
:B)to provide a default value.
Let’s try something more complicated. Say we need to maintain a collection of players and all their game scores. Thiscould be represented by a Map[String, Seq[Int]]
ES6
const scores = {};
function addScore(player, score) {
if (scores[player] === undefined)
scores[player] = [];
scores[player].push(score);
}
function bestScore() {
let bestScore = 0;
let bestPlayer = "";
for (let player in scores) {
const max = scores[player].reduce((a, score) =>
Math.max(score, a)
);
if (max > bestScore) {
bestScore = max;
bestPlayer = player;
}
}
return [bestPlayer, bestScore];
}
function averageScore() {
let sum = 0;
let count = 0;
for (let player in scores) {
for (let score of scores[player]) {
sum += score;
count++;
}
}
if (count == 0)
return 0;
else
return Math.round(sum / count);
}
Scala
import scala.collection.mutable
val scores =
mutable.Map.empty[String, mutable.Buffer[Int]]
def addScore(player: String, score: Int): Unit = {
scores.getOrElseUpdate(player, mutable.Buffer())
.append(score)
}
def bestScore: (String, Int) = {
val all = scores.toList.flatMap {
case (player, pScores) =>
pScores.map(s => (player, s))
}
if (all.isEmpty)
("", 0)
else
all.maxBy(_._2)
}
def averageScore: Int = {
val allScores = scores.flatMap(_._2)
if (allScores.isEmpty)
0
else
allScores.sum / allScores.size
}
In the example above the both versions are using mutable collections. Coming from JavaScript it’s good to start with themore familiar mutable collections, but over time Scala developers tend to favor immutable versions. Immutablecollections in Scala use structural sharing to minimize copying and to provide good performance. Sharing is ok, becausethe data is immutable!
The best score is found by first flattening the whole structure into a sequence of (player, score) pairs. Then we usethe maxBy
:A) method to find the maximum score by looking at the secondvalue in the tuple.
The average is calculated simply by flattening all scores into a single sequence and then calculating its average.
Set
A Set
is like a Map
without values, just the distinct keys. In JavaScript it’s typical toemulate a Set by storing the values as keys into an Object
. This of course means that the values must be converted tostrings. In ES6 there is a new Set
type that works with all kinds of value types, but like with Map
, it’sbased on reference equality, making it less useful when dealing with complex value types.
As their name implies, sets have no duplicate elements. Adding values to a setautomatically guarantees that all duplicate values are eliminated.
Set operations like diff
:This),intersect
:Repr) andunion
:This) allow you to build new sets out of othersets to check, for example, what has changed.
Scala
val set1 = Set(1, 2, 3, 4, 5)
val set2 = Set(2, 3, 5, 1, 6)
val addedValues = set2 diff set1 // Set(6)
val removedValues = set1 diff set2 // Set(4)
Note how in Scala you can also omit the .
and parentheses in method calls.
Sets are also a convenient way to check for multiple values in methods like filter
.
ES6
const common = {"a": true, "the": true,
"an": true, "and": true};
const text = "The sun is a star and an energy source"
const words = text.split(" ")
.map(s => s.toLowerCase())
.filter(s => !common[s]);
Scala
val common = Set("a", "the", "an", "and")
val text = "The sun is a star and an energy source"
val words = text.split(" ")
.map(_.toLowerCase)
.filterNot(common)
// Array(sun, is, star, energy, source)
Next, let’s look at some more advanced paradigms and features of Scala.