This lesson covers:
Scala provides some nice collections.
See Also Effective Scala has opinions about how to use collections.
val numbers = List(1, 2, 3, 4)
Sets have no duplicates
Set(1, 1, 2)
A tuple groups together simple logical collections of items without using a class.
val hostPort = ("localhost", 80)
Unlike case classes, they don't have named accessors, instead they have accessors that are named by their position and is 1-based rather than 0-based.
hostPort._1
hostPort._2
Tuples fit with pattern matching nicely.
hostPort match {
case ("localhost", port) => ...
case (host, port) => ...
}
Tuple has some special sauce for simply making Tuples of 2 values: ->
1 -> 2
(1,2)
See Also Effective Scala has opinions about destructuring bindings ("unpacking" a tuple).
It can hold basic datatypes.
Map(1 -> 2)
Map("foo" -> "bar")
This looks like special syntax but remember back to our discussion of Tuple that ->
can be use to create Tuples.
Map()
also uses that variable argument syntax we learned back in Lesson #1: Map(1 -> "one", 2 -> "two")
which expands into Map((1, "one"), (2, "two"))
with the first element being the key and the second being the value of the Map.
Maps can themselves contain Maps or even functions as values.
Map(1 -> Map("foo" -> "bar"))
Map("timesTwo" -> { timesTwo(_) })
Option
is a container that may or may not hold something.
The basic interface for Option looks like:
trait Option[T] {
def isDefined: Boolean
def get: T
def getOrElse(t: T): T
}
Option itself is generic and has two subclasses: Some[T]
or None
Let's look at an example of how Option is used:
Map.get
uses Option
for its return type. Option tells you that the method might not return what you're asking for.
val numbers = Map(1 -> "one", 2 -> "two")
numbers.get(2)
numbers.get(3)
Now our data appears trapped in this Option
. How do we work with it?
A first instinct might be to do something conditionally based on the isDefined
method.
// We want to multiply the number by two, otherwise return 0.
val result = if (res1.isDefined) {
res1.get * 2
} else {
0
}
We would suggest that you use either getOrElse
or pattern matching to work with this result.
getOrElse
lets you easily define a default value.
val result = res1.getOrElse(0) * 2
Pattern matching fits naturally with Option
.
val result = res1 match {
case Some(n) => n * 2
case None => 0
}
See Also Effective Scala has opinions about Options.
List(1, 2, 3) map squared
applies the function squared
to the elements of the the list, returning a new list, perhaps List(1, 4, 9)
. We call operations like map
combinators. (If you'd like a better definition, you might like Explanation of combinators on Stackoverflow.) Their most common use is on the standard data structures.
Evaluates a function over each element in the list, returning a list with the same number of elements.
numbers.map((i: Int) => i * 2)
or pass in a partially evaluated function
def timesTwo(i: Int): Int = i * 2
numbers.map(timesTwo _)
foreach is like map but returns nothing. foreach is intended for side-effects only.
numbers.foreach((i: Int) => i * 2)
returns nothing.
You can try to store the return in a value but it'll be of type Unit (i.e. void)
val doubled = numbers.foreach((i: Int) => i * 2)
removes any elements where the function you pass in evaluates to false. Functions that return a Boolean are often called predicate functions.
numbers.filter((i: Int) => i % 2 == 0)
def isEven(i: Int): Boolean = i % 2 == 0
numbers.filter(isEven _)
zip aggregates the contents of two lists into a single list of pairs.
List(1, 2, 3).zip(List("a", "b", "c"))
partition
splits a list based on where it falls with respect to a predicate function.
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
numbers.partition(_ %2 == 0)
find returns the first element of a collection that matches a predicate function.
numbers.find((i: Int) => i > 5)
drop
drops the first i elements
numbers.drop(5)
dropWhile
removes the first elements that match a predicate function. For example, if we dropWhile
odd numbers from our list of numbers, 1
gets dropped (but not 3
which is "shielded" by 2
).
numbers.dropWhile(_ % 2 != 0)
numbers.foldLeft(0)((m: Int, n: Int) => m + n)
0 is the starting value (Remember that numbers is a List[Int]), and m acts as an accumulator.
Seen visually:
numbers.foldLeft(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }
Is the same as foldLeft except it runs in the opposite direction.
numbers.foldRight(0) { (m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }
flatten collapses one level of nested structure.
List(List(1, 2), List(3, 4)).flatten
flatMap is a frequently used combinator that combines mapping and flattening. flatMap takes a function that works on the nested lists and then concatenates the results back together.
val nestedNumbers = List(List(1, 2), List(3, 4))
nestedNumbers.flatMap(x => x.map(_ * 2))
Think of it as short-hand for mapping and then flattening:
nestedNumbers.map((x: List[Int]) => x.map(_ * 2)).flatten
that example calling map and then flatten is an example of the "combinator"-like nature of these functions.
See Also Effective Scala has opinions about flatMap.
Now we've learned a grab-bag of functions for working with collections.
What we'd like is to be able to write our own functional combinators.
Interestingly, every functional combinator shown above can be written on top of fold. Let's see some examples.
def ourMap(numbers: List[Int], fn: Int => Int): List[Int] = {
numbers.foldRight(List[Int]()) { (x: Int, xs: List[Int]) =>
fn(x) :: xs
}
}
ourMap(numbers, timesTwo(_))
Why List[Int]()
? Scala wasn't smart enough to realize that you wanted an empty list of Ints to accumulate into.
All of the functional combinators shown work on Maps, too. Maps can be thought of as a list of pairs so the functions you write work on a pair of the keys and values in the Map.
val extensions = Map("steve" -> 100, "bob" -> 101, "joe" -> 201)
Now filter out every entry whose phone extension is lower than 200.
extensions.filter((namePhone: (String, Int)) => namePhone._2 < 200)
Because it gives you a tuple, you have to pull out the keys and values with their positional accessors. Yuck!
Lucky us, we can actually use a pattern match to extract the key and value nicely.
extensions.filter({case (name, extension) => extension < 200})
Why does this work? Why can you pass in a partial pattern match?
Stay tuned for next week!