The latest instance of the course can be found at: O1: 2024
Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.
Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit.
Kieli vaihtuu A+:n sivujen yläreunan painikkeesta. Tai tästä: Vaihda suomeksi.
Chapter 9.2: Pairs and Maps
About This Page
Questions Answered: Can a function return two values? How can I easily find an object that corresponds to another object (e.g., the English translation of a Finnish word)? What’s an easy and efficient way to find elements in a collection when I have a search key of some sort (e.g., to find a student object among many others when I have their student ID number)?
Topics: Pairs, tuples. Key–value pairs. Maps.
What Will I Do? Read and work on small assignments.
Rough Estimate of Workload:? Two hours.
Points Available: B25.
Related Modules: Miscellaneous.
Introduction: Two Return Values?
Frequently asked question
Can a function return two values? Could we, for instance, define a function toMinsAndSec
that takes in a number of seconds and returns two integers: the number of whole minutes
and the leftover seconds? Perhaps it would work like this:
toMinsAndSecs(187)res0: (Int, Int) = (3,7)
It would be nice if we could also save the returned values for later use in two separate
variables; say, min
and s
.
Possible solutions
Functions can’t really return more than a single value, but we can work around that restriction so that it’s as if they could.
One workaround is to return a collection of two elements, say, a Vector[Int]
.
val returnedValues = toMinsAndSecs(187)returnedValues: Vector[Int] = Vector(3,7) val min = returnedValues(0)min: Int = 3 val s = returnedValues(1)s: Int = 7
This solution isn’t terrible, but the type Vector[Int]
doesn’t express the fact that
were always returning exactly two numbers. That solution is also impractical if you wish
to return two values of different types.
Another workaround is to write a custom class that represents the sort of composite
information that the function returns. For instance, we might define a Time
class
with methods for minutes and seconds, and have our function return a Time
object.
This can be a good choice in case the returned information corresponds to a concept
and can be naturally represented as a class.
A third workaround makes use of pairs (pari). Pairs are not only good for returning multiple values; you’ll see in this chapter that they have other uses as well.
Let’s have a look at pairs in general before we return to minutes and seconds.
Pairs
Basic use of pairs in Scala
You can form a pair with brackets and a comma, as shown here:
("good thing", "another thing")res1: (String, String) = (good thing,another thing)
Here’s a familiar-looking way to access the members of a pair:
var myPair = ("good thing", "another thing")myPair: (String, String) = (good thing,another thing) myPair(0)res2: String = good thing myPair(1)res3: String = another thing
A key difference between a pair and, say, a two-element vector is that a pair’s members can have different static types:
val nameAndShoeSize = ("Juha", 43)nameAndShoeSize: (String, Int) = (Juha,43)
Another significant difference is that the size of a pair is known in advance. This attempt produces a compile-time error and thus cannot crash a program at runtime:
nameAndShoeSize(2)-- Error: nameAndShoeSize(2) ^
It’s easy to copy the two members of a pair into two separate variables. Just use brackets:
val (name, shoeSize) = nameAndShoeSizename: String = Juha shoeSize: Int = 43
A minute assignment
Implement the toMinsAndSecs
function we contemplated earlier. It should work like this:
toMinsAndSecs(187)res4: (Int, Int) = (3,7) val (min, s) = toMinsAndSecs(200)min: Int = 3 s: Int = 20
Write your solution in o1/misc.scala
in module Miscellaneous.
A+ presents the exercise submission form here.
Dividing collections
Pairs feature in various API methods. There are a couple of examples below.
Collections have a splitAt
method that returns a pair whose members are collections:
val temperatures = Vector(3, 6, -1, -5, 3, -1, 2)temperatures: Vector[Int] = Vector(3, 6, -1, -5, 3, -1, 2) temperatures.splitAt(3)res5: (Vector[Int], Vector[Int]) = (Vector(3, 6, -1),Vector(-5, 3, -1, 2)) val (workingWeek, weekend) = temperatures.splitAt(5)workingWeek: Vector[Int] = Vector(3, 6, -1, -5, 3) weekend: Vector[Int] = Vector(-1, 2)
splitAt
takes in a number that indicates a position in the
collection.
The first member of the returned pair lists all the elements that are located before the indicated position in the original collection. The second member lists the rest of the elements.
partition
also divides the elements of the original collection in two and returns a
pair of collections:
val (freezing, aboveFreezing) = temperatures.partition( _ < 0 )freezing: Vector[Int] = Vector(-1, -5, -1) aboveFreezing: Vector[Int] = Vector(3, 6, 3, 2)
As you can see, this method divides the elements by calling the given function on each
one and checking whether it returns true
or false
.
Mini-assignment: dividing a collection
Go to package o1.people
in module Miscellaneous and find YoungAndOldApp
. The comments
in this app object explain how the program should work. Implement the missing parts of
the program. Use one of the methods just introduced.
A+ presents the exercise submission form here.
Zipping collections together
You just saw some pairs whose members were collections. You can also have a collection whose elements are pairs.
Collections have a handy method named zipWithIndex
. It forms a new collection where
each element is the original element paired with its index:
val species = Vector("llama", "alpaca", "vicuña")species: Vector[String] = Vector(llama, alpaca, vicuña)
species.zipWithIndexres6: Vector[(String, Int)] = Vector((llama,0), (alpaca,1), (vicuña,2))
We get a vector of pairs, each of which contains a string and an integer.
When you loop over such a collection of pairs, you can again use round brackets to deal with two variables at once. Here are two ways of printing out a collection of pairs:
for pair <- species.zipWithIndex do println(pair(0) + " is at index #" + pair(1))llama is at index #0 alpaca is at index #1 vicuña is at index #2 for (element, index) <- species.zipWithIndex do println(element + " is at index #" + index)llama is at index #0 alpaca is at index #1 vicuña is at index #2
zipWithIndex
is a special case of the more generic zip
method, which pairs up the
elements of two collections:
val heights = Vector(120, 90, 80)heights: Vector[Int] = Vector(120, 90, 80) val heightsAndSpecies = heights.zip(species)heightsAndSpecies: Vector[(Int, String)] = Vector((120,llama), (90,alpaca), (80,vicuña))
If the collections have different lengths, the longer collection’s tail end is ignored:
val moreSpecies = Vector("llama", "alpaca", "vicuña", "guanaco", "wooly")moreSpecies: Vector[String] = Vector(llama, alpaca, vicuña, guanaco, wooly) val justThreePairs = heights.zip(moreSpecies)justThreePairs: Vector[(Int, String)] = Vector((120,llama), (90,alpaca), (80,vicuña))
You can also unzip
a collection of pairs to obtain a pair of collections:
heightsAndSpecies.unzipres7: (Vector[Int], Vector[String]) = (Vector(120, 90, 80),Vector(llama, alpaca, vicuña))
A pair as a parameter
Naturally, you can also define a function that takes a pair as a parameter. The example function below takes a pair of numbers and returns the absolute value of their difference.
def absDiff(pairOfNumbers: (Int, Int)) =
(pairOfNumbers(0) - pairOfNumbers(1)).abs
Usage example:
val myPair = (10, 30)myPair: (Int, Int) = (10,30) absDiff(myPair)res8: Int = 20 absDiff((-300, 100))res9: Int = 400
This works, too:
absDiff(-300, 100)res10: Int = 400
We’ve left out the extra pair of brackets that defines a pair. We’re
invoking the single-parameter function absDiff
as though it took
two separate Int
parameters. Scala automatically constructs a pair
from the two values, so this does the same as absDiff((-300, 100))
.
A similar principle is at work in the following example.
Combining pairs with higher-order functions
Let’s use this familiar example data from above:
val species = Vector("llama", "alpaca", "vicuña")species: Vector[String] = Vector(llama, alpaca, vicuña) val heights = Vector(120, 90, 80)heights: Vector[Int] = Vector(120, 90, 80) val heightsAndSpecies = heights.zip(species)heightsAndSpecies: Vector[(Int, String)] = Vector((120,llama), (90,alpaca), (80,vicuña))
One way to print out a report of each species is this:
def heightDescription(heightSpeciesPair: (Int, String)) = "a typical " + heightSpeciesPair(1) + " is " + heightSpeciesPair(0) + " cm at the shoulder"def heightDescription(heightSpeciesPair: (Int, String)): String heightsAndSpecies.map(heightDescription).foreach(println)a typical llama is 120 cm at the shoulder a typical alpaca is 90 cm at the shoulder a typical vicuña is 80 cm at the shoulder
Since the collection contains pairs, we give its map
method a
function that takes in a pair and determines how to report it.
Since Scala lets us use a two-parameter function in place of a function that takes a single pair, we can alternatively write this:
def heightDescription(cm: Int, species: String) = s"a typical $species is $cm cm at the shoulder"def heightDescription(cm: Int, species: String): String heightsAndSpecies.map(heightDescription).foreach(println)a typical llama is 120 cm at the shoulder a typical alpaca is 90 cm at the shoulder a typical vicuña is 80 cm at the shoulder
We write a perfectly normal two-parameter function that takes in a number and a string.
We may pass that function to the map
method of a collection
that consists of pairs. Scala ensures that our two-parameter
function gets invoked on the members of each pair in the
collection.
Assignment: isAscending
Write a function isAscending
that takes in a vector of integers and reports whether those
integers are in ascending order. Here are some examples:
isAscending(Vector(-10, 1, 5, 10))res11: Boolean = true isAscending(Vector(-10, 1, 5, 5, 10))res12: Boolean = true isAscending(Vector(-10, 1, 5, 9, 5, 10))res13: Boolean = false isAscending(Vector(10, 5, 1, -10))res14: Boolean = false
Your method must not do extra work by actually sorting the collection. It should merely check if the elements are already sorted.
With clever use of the collections methods from this chapter and earlier ones, you can solve the assignment in a single, short line of code.
Write your code in misc.scala
again.
You may assume that the vector contains at least one element.
A detailed hint on which methods to use
You may find the methods zip
, tail
, and forall
useful.
(Alternatively, you could try using the sliding
method
introduced in 7.1’s drySpell
assignment.)
You can use forall
to check whether it’s true, for each
pair of consecutive elements, that they are in the right order.
The auxiliary function that you pass to forall
you could
either def
ine as a named function or type in as an anonymous
function literal.
A+ presents the exercise submission form here.
Pairs are tuples
A pair is a two-membered tuple (monikko). A tuple may have more members than just two:
("This tuple has four members of different types.", 100, 3.14159, false)res15: (String, Int, Double, Boolean) = (This tuple has four members of different types.,100,3.14159,false)
Does that mean a tuple is a collection, like buffers and vectors are? Not quite.
First of all, tuples and collections have different purposes. Tuples tend to be most useful when: 1) there is a small, constant number of members (e.g., only two), and 2) each member has a meaning separate from the others (e.g., one signifies minutes, the other seconds; one is a name, the other is a shoe size), but 3) it nevertheless feels like overkill to write a custom class.
Here are some of the technical differences between tuples and collections:
In a tuple, each member has a distinct static type. In a collection, the elements share a static type.
Tuples are always immutable in size and content. Collections come in both mutable and immutable varieties.
Tuples don’t have the familiar collection methods (
size
,map
,indexOf
, etc.) and you can’t use afor
loop to iterate over a tuple (without resorting to additional trickery).
Tuples and case classes
The optional material in Chapter 4.4 briefly introduced Scala’s
case class
construct. In a sense, a case class is a sort of
customized tuple. For more on case classes, see the spring course
Programming Studio 2.
Another (old) way of accessing members of tuples
When you venture beyond this ebook, you may run into this sort of code:
val nameAndNumber = ("Juha", 43)nameAndNumber: (String, Int) = (Juha,43) nameAndNumber._1res16: String = Juha nameAndNumber._2res17: Int = 43
The first member of a tuple has the name _1
. That’s
one, not zero! The second member is _2
(and so forth).
Here, the underscore doesn’t have any special meaning. It’s simply a part of a method’s name. In Scala, like in many other languages, the name of a method can’t start with a digit; the underscores have been used for that reason. This use of the underscore character has nothing to do with compacting function literals (cf. Chapter 6.2).
In older versions of Scala, one couldn’t write things like nameAndNumber(0)
to access a tuple, so the notation with underscores was more common.
One massively helpful and common thing to do with tuples — pairs, specifically — is to construct “maps” out of them. We’ll say more about that in just a bit.
Another Introduction: Keys and Values
How would you implement the following? What do all these scenarios have in common?
A dictionary: given a word in Finnish, we want to be able to find the corresponding word in English.
A student register: we want to be able to search for students by their student ID number.
An address book: we want to be able to find the person object that details an individual whose name we know.
An encyclopedia: given a string keyword, we want to access the encyclopedia article that matches that keyword.
Counting frequencies: We have a bunch of objects that all have a particular property. We want to know the distribution of different values of that property among the objects.
For instance, consider a theme we already touched on in Chapter 1.6: we have a list of top-rated movies, each of which has a director (or several), and we want to find out how many times each director features in the list.
It’s possible to solve these problems with the tools we’ve already covered. For example, here’s a buffer-based implementation for a primitive sort of dictionary:
val searchTerms = Buffer("kissa", "laama", "tapiiri", "koira")searchTerms: Buffer[String] = ArrayBuffer(kissa, laama, tapiiri, koira) val englishTranslation = Buffer("cat", "llama", "tapir", "puppy")englishTranslation: Buffer[String] = ArrayBuffer(cat, llama, tapir, puppy)
We can use these two buffers to look up the translation of a Finnish word. (Recall the
indexOf
method from Chapter 4.2.)
val index = searchTerms.indexOf("tapiiri")index: Int = 2 englishTranslation(index)res18: String = tapir englishTranslation(searchTerms.indexOf("koira"))res19: String = puppy
We can also update the dictionary by replacing an element. The Finnish word “koira” is better translated as “dog” than “puppy”, so let’s make that change:
englishTranslation(searchTerms.indexOf("koira")) = "dog"englishTranslation(searchTerms.indexOf("koira"))res20: String = dog
Pause to consider what we’re doing here. Our dictionary contains Finnish words, which
we use to look up the corresponding English words. Here’s one way to put it: the Finnish
words in searchTerms
serve as keys (avain) that we use to locate the values
(arvo) stored in englishTranslation
.
The same basic pattern repeats in all the other scenarios listed above: student ID numbers as keys, the student objects as values; director names as keys, the movie counts of the directors as values; etc. In the following text, we will use the terms “key” and “value” in this sense.
Our buffer-based solution relies on the assumption that the two buffers store their elements
in a specific order and the indices of the keys (in searchTerms
) are the same as those
of the corresponding values (in englishTranslation
). The solution does work, but it’s
not ideal:
We used two separate buffers for keys and values even though we conceive of the dictionary as a single entity.
It feels unnecessary to have to mess about with indices in order to look up a word. Nobody really cares which index the source or target word has in the buffer.
What if the key is not found?
E.g.,
englishTranslation(searchTerms.indexOf("Mikki-Hiiri"))
.indexOf
will then return -1 and we end up with a runtime error by accessing a negative index ofenglishTranslation
as we’ve done.
(If we had lots of words in the dictionary, we might also wish for improved efficiency.
indexOf
checks each index of the buffer in order, one by one, until it reaches the target element. This search strategy isn’t even close to optimal.)
Maps
Let’s use a collection that stores both keys and values. A map (hakurakenne) is a tremendously useful and common collection type, and rather unlike the index-based collections that we’ve used so far in O1.
A map contains key–value pairs. When you use, say, a buffer, you look up elements by their positions in the collection; when you use a map, you instead look up values by their keys.
You can use any objects as keys. Those objects form a set in the mathematical sense of the word; in practice, that means that all of a map’s keys must be different from each other (e.g., no identical Finnish words as search terms, no identical student ID numbers, etc.).
In the Scala API, maps have been implemented as class Map
.
Hold up! map
is a method on collection objects, right — not a type of collection?
It’s true that we’ve been already using a method named map
. The
two concepts are distinct but there is a reason why their names are
similar.
The familiar map
-that-is-a-method takes an existing collection
and uses its elements to generate another. The parameter function
of map
defines a mapping from the original elements to the
generated elements.
A Map
object — a map-that-is-a-collection — is a collection
of keys and values; it stores a mapping from keys to values.
The map
method is available on all sorts of collections, not just
on Map
s.
But Before We Get to Map
s: Key–Value Pairs
Here’s some Scala code that defines a key–value pair. This is just a regular pair that happens to contain data that we intend to use as a key and a value.
("koira", "dog")res21: (String, String) = (koira,dog)
However, when defining key–value pairs, it’s common to use another notation. This other notation highlights the fact that each key “points at” the corresponding value. The following code produces precisely the same outcome as the code above:
"koira" -> "dog"res22: (String, String) = (koira,dog)
The ->
arrow defines a pair.
We put the key on the left and the value on the right.
We get a pair of data items.
Either notation works for creating any pair. In O1, we’ll prefer the arrow notation when
we define pairs that represent a Map
’s keys and values.
You can use any objects in a key–value pair. In the above example, both the key and the value were strings, as befits our dictionary. It is equally possible to, say, associate an integer key with a string value or the other way around:
233524 -> "Susan Student"res23: (Int, String) = (233524,Susan Student) "Sergio Leone" -> 5res24: (String, Int) = (Sergio Leone,5)
Note that the arrow ->
that we’re using here to construct pairs is not the same as the
arrow =>
that we’ve used in function types and case
branches.
I’m starting to get my arrows all mixed up.
More about the arrow
->
is an ordinary method.
"seikkailija" -> "adventurer"res25: (String, String) = (seikkailija,adventurer)
The target object whose method we call.
This method, whose name consists of a hyphen and a greater-than sign, is available on all Scala objects. Here, we have used operator notation (Chapter 5.2), which omits the period-dot between the target object and the method name.
The second string is passed as a parameter to the method. In the operator notation, we can omit the brackets around the parameter.
The method returns a pair that comprises the target object and the parameter object.
We could have used the dot notation. These two expressions are equivalent:
"polvi".->("knee")res26: (String, String) = (polvi,knee) "polvi" -> "knee"res27: (String, String) = (polvi,knee)
Since the idea is to visually suggest an arrow, the operator notation is understandably much more common here.
Maps in Scala: Class Map
It’s straightforward to implement a little Finnish-to-English dictionary as a Map
.
First, we’ll need to import
the Map
class that we intend to use:
import scala.collection.mutable.Map
As you’ll know by now, objects can be mutable or immutable. Since Scala is designed
to support different styles of programming, its standard API comes with two different
Map
classes, one for mutable maps and one for immutable ones. For now, we’ll mostly use
mutable maps: map objects whose keys and values can be modified. The corresponding class
is in package scala.collection.mutable
and needs to be import
ed as we did above.
We can create a Map
object like this:
val finnishToEnglish = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")finnishToEnglish: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
The difference to the familiar collection types is that a map’s elements are always key–value pairs.
A map has two type parameters: the type of the keys and the type of the values. In this example, we have a map whose keys and values are both strings.
A look at the REPL output is enough to suggest that this isn’t a numerically indexed collection. The elements are all over the map in what looks like an arbitrary order. They have no indices. The logic that dictates how a map orders its contents depends on the map’s internal implementation and is unimportant for present purposes.
The sections below demonstrate a selection of the methods available on Map
objects.
Once again, the intention is not that you memorize them all but that you get an overall
sense of what you can do with this collection type.
As you read these examples, you’ll see that there are (again) a number of ways to do the same thing. It’s unlikely that you’ll be able to tell right away which solution is the best fit for which situation. Then again, you don’t need to; that’s the sort of thing that improves with experience. It’s good to learn to recognize different kinds of solutions already, though.
Accessing a single value
It’s simple to fetch a value that matches a key:
finnishToEnglish.get("kissa")res28: Option[String] = Some(cat) finnishToEnglish.get("Mikki-Hiiri")res29: Option[String] = None
The get
method takes a key as a parameter.
Its return type is Option[TypeOfValuesInMap]
.
If the key was present in the map, we get
a Some
that holds the corresponding value.
If not, we get None
.
Replacing a value
When working with a mutable map, we can update it as shown below.
finnishToEnglish.get("koira")res30: Option[String] = Some(puppy) finnishToEnglish("koira") = "dog"finnishToEnglish.get("koira")res31: Option[String] = Some(dog)
Remember: the keys of a map are unique. A map cannot contain multiple key–value pairs
whose keys are equal. So what happens above is that the pair "koira" -> "puppy"
is
replaced by the pair "koira" -> "dog"
.
Adding a key–value pair
There are a number of ways to add a key–value pair to a mutable map. One of them is to assign a value to a previously unused key:
finnishToEnglish("hiiri") = "mouse"
Another way to add or replace a key–value pair is the +=
operator, which you’ve
previously used on buffers:
finnishToEnglish += "sika" -> "pig"res32: Map[String, String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat, sika -> pig, hiiri -> mouse, laama -> llama)
This command also replaces any previously stored key–value pair that has the same key.
The rest of this ebook uses this second notation to add pairs to a map. You can use whichever you prefer.
Removing a key–value pair
Here’s one way to remove a pair:
finnishToEnglish -= "laama"res33: Map[String, String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat, sika -> pig, hiiri -> mouse)
There’s also a remove
method that returns the removed value in an Option
wrapper:
finnishToEnglish.remove("laama")res34: Option[String] = Some(laama)
Checking a key
The contains
method tells us if a given key is present in the map. In the example
below, "tapiiri"
is present in the dictionary whereas "laama"
, which we just
removed, is not.
finnishToEnglish.contains("tapiiri")res35: Boolean = true finnishToEnglish.contains("laama")res36: Boolean = false
Getting all the keys or all the values
The methods keys
and values
return all the keys and all the values of a map,
respectively. These methods return collections that you can process with the usual
collection methods, such as foreach
. Here, for instance, we first print out all the
keys followed by all the values.
finnishToEnglish.keys.foreach(println)koira tapiiri kissa sika hiiri finnishToEnglish.values.foreach(println)dog tapir cat pig mouse
Familiar methods on Map
s
Map
s are collections and, as such, have many familiar methods. Here are just a few
examples of the collection methods from Chapters 4.2 and 6.3:
val finToEng = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog")finToEng: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) finToEng.isEmptyres37: Boolean = false finToEng.sizeres38: Int = 3 finToEng.foreach(println)(koira,dog) (tapiiri,tapir) (kissa,cat)
The map
method (lower case) is worth a special mention. You can use it to process the
key–value pairs of a Map
(upper case) in order to create a new Map
:
val myTestMap = finToEng.map( finEngPair => finEngPair(0).toUpperCase -> (finEngPair(1) + "!!!") )myTestMap: Map[String,String] = Map(KISSA -> cat!!!, TAPIIRI -> tapir!!!, KOIRA -> dog!!!)
Since a Map
consists of pairs, we pass the map
method
a function that operates on pairs.
Our example function forms a new pair that consists of an upper-case version of the original pair’s first member (the Finnish key)...
... and a string consisting of the original pair’s second member (the English value) and some exclamation marks.
These new pairs form a new Map
, which maps upper-case
Finnish words to shouty English words.
We can write that code to be easier to read and more elegant. Let’s make use of something that already came up in this chapter: Scala lets us use a two-parameter function in place of a function that takes a single pair as a parameter. Like so:
val myTestMap = finToEng.map( (fin, eng) => fin.toUpperCase -> (eng + "!!!") )myTestMap: Map[String,String] = Map(KISSA -> cat!!!, TAPIIRI -> tapir!!!, KOIRA -> dog!!!)
We use a two-parameter function literal whose parameters have descriptive names. There’s no need to mess about with indices.
In many programs, it is useful to construct a new Map
whose keys are identical to an
existing Map
’s keys but whose values are different from the original map’s. As a little
example, let’s construct a Map
from Finnish words to the lengths of their English
translations’:
val lengthOfEnglish = finToEng.map( (fin, eng) => fin -> eng.length )lengthOfEnglish: Map[String,String] = Map(kissa -> 3, tapiiri -> 5, koira -> 3)
We give the map
method a function that leaves the
first member of each key–value pair untouched.
Flipping a Map
A frequently asked question: how can you make a Map
whose keys
are the values of an existing Map
and whose values are the
original Map
’s keys?
In simple cases, that’s easy enough to do, especially if you use
the swap
method that’s available on pairs and works like this:
val myPair = "key" -> "value"myPair: (String, String) = (key,value) myPair.swapres39: (String, String) = (value,key)
Here’s the swap
operation applied to an entire Map
:
val finnishToEnglish = Map("kissa" -> "cat", "koira" -> "dog", "laama" -> "llama")finnishToEnglish: Map[String,String] = Map(kissa -> cat, koira -> dog, laama -> llama)
val englishToFinnish = finnishToEnglish.map( _.swap )englishToFinnish: Map[String,String] = Map(cat -> kissa, dog -> koira, llama -> laama)
We call swap
on every key–value pair.
If you do something like this, watch out: the keys of a map are unique. If our mini-dictionary had contained multiple words that translate to the same English word, we would have lost information while swapping. (Try it.)
If you want a key to be associated with multiple values, you can pair the key with a collection that holds all those values. You may also want to explore existing libraries. (Search for scala multidict.)
What If You Don’t Find a Key?
When you use a map, you often need to be alert to the possibility that the key you’re looking for is not present. There are several alternative ways of dealing with that contingency.
You already know that a Map
’s get
method returns an Option
, which is None
in case
the key wasn’t found:
finToEng.get("Mikki-Hiiri")res40: String = None
Often, you’ll do fine calling get
and using the Option
it returns. But depending on
what you’re trying to achieve, there may be better alternatives.
The getOrElse
method
One way to deal with missing values is to use getOrElse
instead. This method greatly
resembles the method of the same name on Option
s (Chapter 4.3).
On Option
s, the getOrElse
method essentially tells the Option
object: “Give me
the value you contain or, failing that, use this value instead.” The getOrElse
method
of a Map
similarly tells the Map
: “Give me the value that corresponds to this key or,
failing that, use this value instead.”
finToEng.getOrElse("kissa", "unknown word")res41: String = cat finToEng.getOrElse("Mikki-Hiiri", "unknown word")res42: String = unknown word
We give getOrElse
the search key and a “default value”
that should match the type of the Map
’s values.
If the key is present in the Map
, getOrElse
returns the
corresponding value.
If the key isn’t present, the method returns the value of its second parameter.
The return type is String
rather than Option[String]
.
We don’t need an Option
wrapper since we have a meaningful
String
value in both cases. Lovely.
The default method apply
(and its dangers)
You’re already used to indexing collections with expressions like myCollection(index)
,
which Chapter 5.3 revealed to be shorthand for method calls like myCollection.apply(index)
.
As you learned then, apply
is a sort of implicit “default method” for objects.
Map
s, too, have an apply
method. It works much like get
, but there is a
substantial difference. Compare:
val engToFin = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog")engToFin: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) engToFin("tapiiri")res43: String = tapir engToFin("Mikki-Hiiri")java.util.NoSuchElementException: key not found: Mikki-Hiiri ... at scala.collection.mutable.HashMap.apply(HashMap.scala:64) ... engToFin.get("kissa")res44: Option[String] = Some(cat) engToFin.get("Mikki-Hiiri")res45: Option[String] = None
The nice thing about apply
, compared to get
, it gives you a plain result without an
Option
, so the return value is a little bit easier to work with. The not-nice-at-all
thing about apply
is that it causes a runtime error if you pass in a non-existent key,
so you need to be extra careful when you use this method.
get
is usually the better and safer choice.
Practice on get
, apply
, and getOrElse
A Map
in an Instance Variable
You can use Map
s as building blocks for your own classes. A simple example is shown
below; you’ll see additional examples in later chapters.
Example: societies and their members
We’ll use the Member
class from Chapter 4.4 as part of this example. Here is the
relevant code:
class Member(val id: Int, val name: String, val yearOfBirth: Int, val yearOfDeath: Option[Int]):
// etc.
Now, let’s write another class, Society
, that stores the members of a society in an
instance variable that refers to a Map
.
class Society(val name: String):
private val members = Map[Int, Member]()
def add(newMember: Member) =
this.members(newMember.id) = newMember
def getByID(memberID: Int) = this.members.get(memberID)
end Society
Each Society
object has a name. It also has a Map
whose values
are the society’s members and whose keys are the members’ ID
numbers. For a newly created Society
object, this Map
is
empty.
The Society
object has methods for adding members and finding
existing members by their ID. These methods have been implemented
by calling the Map
’s methods.
Mini-assignment: Map
s and Option
You’ll find Society
in module Miscellaneous. Add a yearOfDeath
method to the class.
The method should:
receive a member ID as a parameter;
return an
Option[Int]
that wraps the year of death of the member with the given ID; except thatif the member doesn’t exist or is not dead yet, the method should return
None
.
Don’t make any other changes to Society
. Don’t edit Member
.
Hint: there is a method that supplies a simple solution. It was introduced in a recent chapter.
A+ presents the exercise submission form here.
Optional Reading
Setting a default value on a Map
When we call getOrElse
, we pass in a “default value”, which the
method uses in case the key isn’t found. We can also associate a
Map
with a generic default value. For instance, we might wish our
dictionary to default to the string "not found"
on any look-up of
any non-existent key. The method withDefaultValue
does just that:
val engToFin = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog").withDefaultValue("not found")engToFin: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) engToFin("kissa")res46: String = cat engToFin("Mikki-Hiiri")res47: String = not found
We use withDefaultValue
to indicate which value the map should
default to.
Now when we apply
for a missing key, we get this default value.
What’s the hash?
The “hash” in HashMap
has nothing to do with Twitter or any other
pastimes. It refers to a particular way of implementing Map
s.
In the Scala API, the data type Map
is defined a trait with
multiple abstract methods: it specifies what all Map
s can
do without specifying how any Map
s actually implement those
things. HashMap
, in turn, is a concrete class that implements
the abstract methods of the Map
trait using an implementation
strategy known as hashing (hajautus). The Map
s we’ve
created have had both the general type Map
and its subtype
HashMap
.
This chapter’s introductory section mentioned in passing that the
two-buffer dictionary implementation wasn’t too efficient, because
it processed each index in turn and any key–value pair could
appear just anywhere in the buffers. In a typical scenario, a
HashMap
is more efficient because it organizes the key–value
pairs cleverly so that the key suggests where to find the value.
In O1, we gloss over most efficiency concerns and won’t cover hashing in more detail, but you can read more on Wikipedia, for instance, or take Aalto’s Data Structures and Algorithms course.
Summary of Key Points
Multiple data items — even items of different types — can be combined into a tuple. A pair is a tuple with two members.
Maps are a tremendously useful and common sort of collection. A map contains key–value pairs. It differs from numerically indexed collections in that:
a map’s elements don’t have an order determined by running numbers;
nor can you use indices to select a particular element of a map. Instead,
you commonly use a map via keys: it’s easy and efficient to fetch a value that corresponds to a key.
The Scala API gives us a
Map
class with useful methods.Links to the glossary: tuple, pair; map, key–value pair.
Feedback
Please note that this section must be completed individually. Even if you worked on this chapter with a pair, each of you should submit the form separately.
Credits
Thousands of students have given feedback and so contributed to this ebook’s design. Thank you!
The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.
The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.
The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Anna Valldeoriola Cardó, and Aleksi Vartiainen.
The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.
The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.
The other diagrams and interactive presentations in the ebook are by Juha Sorva.
The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.
The pedagogy of using O1Library for simple graphical programming (such as Pic
) is
inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and
Krishnamurthi and Picturing Programs by Stephen Bloch.
The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.
The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It has been designed and implemented by various students in collaboration with O1’s teachers.
For O1’s current teaching staff, please see Chapter 1.1.
Additional credits appear at the ends of some chapters.
There are brackets in the REPL output, too.
(String, String)
means a pair whose members are both strings.