7. Lists
A variable holds one value. But programs often deal with many values of
the same kind: the scores in a game, the names of your friends, the
temperatures for each day of the week. Making a separate variable for each
one (score1, score2, score3, …) gets out of hand quickly, and you
can’t loop over separate variables.
A list holds any number of values, in order, under one name.
In this lesson you’ll learn:
- how to create a list, including an empty one
- how to read, change, add and remove items
- how to loop over a list
- what happens when you ask for an item that isn’t there
- the most useful built-in list methods:
contains,sorted,reversed,filterandmap - why changing a copy of a list doesn’t change the original
Creating a list
Section titled “Creating a list”Write the items between square brackets, separated by commas:
fn main() { let scores = [72, 95, 58] let names = ["Ada", "Grace", "Linus"] print(scores.count) print(names.count)}33count tells you how many items a list has.
All the items in a list must have the same type. scores is a list of
Ints, and its type is written [Int]. names is a [String]. Tessel
works this out from the items, just as it works out that let age = 36 is
an Int.
Empty lists
Section titled “Empty lists”Often you start with an empty list and fill it as the program runs. An
empty list, [], has no items for Tessel to look at, so you have to write
its type yourself:
fn main() { var shopping: [String] = [] print(shopping.count) print(shopping.isEmpty)}0trueisEmpty is true when the list has no items. It’s a shorter way of
writing shopping.count == 0.
Common mistake: an empty list without a type
Section titled “Common mistake: an empty list without a type”If you leave the type out, Tessel can’t tell what the list will hold:
fn main() { var shopping = [] shopping.append("milk")}error: can't tell what type this empty list is --> main.tsl:2:20 |2 | var shopping = [] | ^^ | = help: give it a type, like `var names: [String] = []`
1 error foundReading items by position
Section titled “Reading items by position”Each item has a position, called its index. Indexes start at 0, not
- So the first item is at index 0, the second at index 1, and the last at
index
count - 1.
To read an item, write the list’s name and the index in square brackets:
fn main() { let names = ["Ada", "Grace", "Linus"] print(names[0]) print(names[1]) print(names[names.count - 1])}AdaGraceLinusStarting at 0 feels strange at first. It helps to think of the index as
“how many items come before this one”: zero items come before "Ada".
Changing a list
Section titled “Changing a list”To change a list, it must be a var. A list declared with let can never
change: not its items, and not its length.
Replacing an item
Section titled “Replacing an item”Assign to an index to replace the item there:
fn main() { var names = ["Ada", "Grace", "Linus"] names[2] = "Margaret" print(names[2])}MargaretAdding items
Section titled “Adding items”append adds an item to the end. insert puts an item before a given
index, and moves the rest along:
fn main() { var queue = ["Ada", "Grace"] queue.append("Linus") queue.insert("Alan", at: 0) for name in queue { print(name) }}AlanAdaGraceLinusA name followed by a dot and a word, like queue.append(…), is a
method: a function that belongs to a value and works on it. count is
similar, but without parentheses, because it just reads a fact about the
list. That kind is called a property.
Removing items
Section titled “Removing items”remove(at:) takes out the item at an index, and moves the later items
back to fill the gap. It also gives you the item it removed, in case you
want it:
fn main() { var queue = ["Alan", "Ada", "Grace", "Linus"] let served = queue.remove(at: 0) print("Now serving {served}") print("{queue.count} still waiting") print("Next up: {queue[0]}")}Now serving Alan3 still waitingNext up: AdaCommon mistake: changing a let list
Section titled “Common mistake: changing a let list”fn main() { let names = ["Ada", "Grace"] names.append("Linus")}error: can't call `append` on `names`, which is a constant, because `append` changes it --> main.tsl:3:5 |3 | names.append("Linus") | ^^^^^ ::: main.tsl:2:9 |2 | let names = ["Ada", "Grace"] | ----- declared here | = help: declare it with `var` to be able to change it
1 error foundLooping over a list
Section titled “Looping over a list”You already know for loops from lesson 5. A for loop over a list runs
its body once for each item, in order:
fn main() { let temperatures = [18, 21, 25, 19] var total = 0 for t in temperatures { total += t } print("Total: {total}")}Total: 83When you need the index as well as the item, loop over the indexes with
0..list.count (from 0 up to, but not including, count), and read each
item with list[i]:
fn main() { let days = ["Mon", "Tue", "Wed"] let temperatures = [18, 21, 25] for i in 0..days.count { print("{i + 1}. {days[i]}: {temperatures[i]} degrees") }}1. Mon: 18 degrees2. Tue: 21 degrees3. Wed: 25 degreesWhen you need each item’s position but only one list, enumerated() gives
both at once:
fn main() { let days = ["Mon", "Tue", "Wed"] for (i, day) in days.enumerated() { print("{i + 1}. {day}") }}1. Mon2. Tue3. Wed(i, day) takes each position and item apart into two names; you’ll see
how this works (it’s a tuple) in lesson 11.
Going out of range
Section titled “Going out of range”What happens if you ask for an index that doesn’t exist? Tessel can’t know ahead of time how long a list will be when the program runs, so this mistake is caught while the program runs. The program stops straight away with a runtime error:
fn main() { let names = ["Ada", "Grace", "Linus"] print(names[0]) print(names[3]) print("done")}Adaerror: index 3 is out of range for a list of 3 items --> main.tsl:4:11The message tells you which index you asked for, how long the list was,
and the line and column where it happened (line 4, column 11). Notice that
"done" was never printed: the program stopped at the error.
A list of 3 items has the indexes 0, 1 and 2. Asking for index 3, one past the end, is the most common way to hit this error. If you’re not sure an index is valid, check it first:
fn main() { let names = ["Ada", "Grace", "Linus"] let wanted = 3 if wanted < names.count { print(names[wanted]) } else { print("There's no item {wanted}") }}There's no item 3The same error happens with remove(at:) and insert(_:at:) when the index
is outside the list.
first and last
Section titled “first and last”first and last give you the first and last item. But an empty list has
neither, so these don’t give a plain value. They give an optional: a
value that might be missing. You’ll learn all about optionals in
lesson 10. For now, the one thing you need is
??, which says “or, if it’s missing, use this instead”:
fn main() { let scores = [72, 95, 58] let empty: [Int] = [] print(scores.first ?? 0) print(scores.last ?? 0) print(empty.first ?? 0)}72580This is safer than scores[0], which would stop the program if the list
were empty.
Useful list methods
Section titled “Useful list methods”Lists come with many built-in methods. Here are the ones you’ll reach for most.
contains
Section titled “contains”contains tells you whether an item is in the list:
fn main() { let guests = ["Ada", "Grace", "Linus"] print(guests.contains("Grace")) print(guests.contains("Bob"))}truefalsesorted and reversed
Section titled “sorted and reversed”sorted() gives you a new list in order, smallest first (or
alphabetical, for text). reversed() gives you a new list back to front.
Neither changes the original list:
fn main() { let scores = [72, 95, 58, 84] let ordered = scores.sorted() let highestFirst = scores.sorted().reversed() print(ordered[0]) print(highestFirst[0]) print(scores[0])
let names = ["Linus", "Ada", "Grace"] print(names.sorted().joined(separator: ", "))}589572Ada, Grace, Linusjoined(separator:) glues a list of strings into one string, with the
separator between the items. It’s the easiest way to show a whole [String]
at once.
filter and map
Section titled “filter and map”filter keeps only the items you want. map turns each item into
something else. Both take a small piece of code in braces, called a
block, which is run for each item:
fn main() { let scores = [72, 95, 58, 84] let passed = scores.filter { s in s >= 60 } let doubled = scores.map { s in s * 2 } print(passed.count) print(doubled[0])}3144Read { s in s >= 60 } as “for each item, call it s, and keep it if
s >= 60”. Read { s in s * 2 } as “for each item s, give back s * 2”.
The name before in is up to you.
Blocks are a big topic of their own, and lesson 13
explains them fully. For now, you can use filter and map with this simple
pattern. Anything they do, you could also do with a for loop, just with
more lines.
There are many more methods, such as sum(), min(), max(),
index(of:) and shuffled(). The List reference
lists them all.
Printing a whole list
Section titled “Printing a whole list”print shows a whole list the way you’d write it in code, and so does
{…} in text:
fn main() { let scores = [72, 95, 58] print(scores) let names = ["Ada", "Alan"] print("Players: {names}")}[72, 95, 58]Players: ["Ada", "Alan"]Text inside a list is shown in quotes, so you can tell ["a b"] (one item)
from ["a", "b"] (two). To show the items your own way, loop over them, or
join a list of strings with joined(separator: ", ").
Lists are values: copies are separate
Section titled “Lists are values: copies are separate”When you assign a list to another variable, you get a copy. Changing the copy doesn’t change the original:
fn main() { let original = ["Ada", "Grace"] var copy = original copy.append("Linus") print(original.count) print(copy.count)}23The same happens when you pass a list to a function: the function gets its own copy. This makes programs easier to reason about. When you hand a list to a function, you know your list is still exactly as you left it. If you want a changed version, have the function return the new list:
fn withBonus(_ scores: [Int]) -> [Int] { var result = scores for i in 0..result.count { result[i] += 5 } result}
fn main() { let scores = [72, 95, 58] let boosted = withBonus(scores) print("{scores[0]} became {boosted[0]}")}72 became 77To learn more about how Tessel copies values, see Values and copying.
Worked example: grades
Section titled “Worked example: grades”Let’s put it together. Given a list of test grades, we want to know the average, the highest grade, and which grades are above average.
We’ll write one function for each question. Each one takes the list as a parameter.
fn average(_ grades: [Int]) -> Float { var total = 0 for g in grades { total += g } Float(total) / Float(grades.count)}
fn highest(_ grades: [Int]) -> Int { var best = grades[0] for g in grades { if g > best { best = g } } best}
fn main() { let grades = [72, 95, 58, 84, 90, 67] let avg = average(grades) print("Grades: {grades.count}") print("Average: {avg.formatted(decimals: 1)}") print("Highest: {highest(grades)}")
let above = grades.filter { g in Float(g) > avg } print("Above average: {above.count}") for g in above { print(" {g}") }}Grades: 6Average: 77.7Highest: 95Above average: 3 95 84 90A few things to notice:
- In
average,totalandgrades.countareInts. Dividing twoInts drops the fraction (lesson 3), so we turn both intoFloats withFloat(…)first. avg.formatted(decimals: 1)shows aFloatwith one digit after the point, so77.66666666666667is shown as77.7.higheststarts with the first grade as the best so far, then checks every grade against it. It’s the “find the largest” pattern from lesson 5: keep the best so far, and update it when you find better.- To compare an
Intgrade with theFloataverage, we turn the grade into aFloattoo.
There’s a problem hiding here, though. What if the list is empty?
highest reads grades[0], which would stop the program with an
out-of-range error. A careful program checks first:
fn main() { let grades: [Int] = [] if grades.isEmpty { print("No grades yet") } else { print("Highest: {grades.max() ?? 0}") }}No grades yetThis version also uses the built-in max(), which finds the largest item
for you. Like first, it gives an optional, because an empty list has no
largest item.
Exercises
Section titled “Exercises”1. Shopping list. Start with an empty [String]. Append "eggs",
"milk" and "bread", then insert "apples" at the front. Print how many
items there are, then each item on its own line.
Solution
fn main() { var shopping: [String] = [] shopping.append("eggs") shopping.append("milk") shopping.append("bread") shopping.insert("apples", at: 0) print("{shopping.count} items:") for item in shopping { print(item) }}4 items:appleseggsmilkbread2. Count the evens. Given let numbers = [3, 8, 12, 7, 6, 1], count
how many are even, first with a for loop, then with filter.
Solution
fn main() { let numbers = [3, 8, 12, 7, 6, 1]
var evens = 0 for n in numbers { if n % 2 == 0 { evens += 1 } } print(evens)
print(numbers.filter { n in n % 2 == 0 }.count)}333. Smallest, by hand. Write fn smallest(_ numbers: [Int]) -> Int that
finds the smallest number with a loop (don’t use min()). You may assume
the list isn’t empty. Test it with [42, 7, 19, 3, 25].
Solution
fn smallest(_ numbers: [Int]) -> Int { var result = numbers[0] for n in numbers { if n < result { result = n } } result}
fn main() { print(smallest([42, 7, 19, 3, 25]))}34. Remove the negatives. Write fn positives(_ numbers: [Int]) -> [Int]
that returns a new list with only the numbers greater than zero. Build the
new list with a loop and append. Check that the original list is
unchanged afterwards.
Solution
fn positives(_ numbers: [Int]) -> [Int] { var result: [Int] = [] for n in numbers { if n > 0 { result.append(n) } } result}
fn main() { let readings = [5, -2, 8, 0, -7, 3] let good = positives(readings) print("{good.count} of {readings.count} readings are positive") for n in good { print(n) }}3 of 6 readings are positive583readings.filter { n in n > 0 } does the same in one line.
5. Running totals. Write fn runningTotals(_ numbers: [Int]) -> [Int].
Each item of the result is the sum of all the numbers up to and including
that position. For [3, 1, 4, 1, 5] the result is [3, 4, 8, 9, 14].
Print the result as 3, 4, 8, 9, 14. (Hint: to join numbers, map them
to strings with { n in "{n}" } first.)
Solution
fn runningTotals(_ numbers: [Int]) -> [Int] { var result: [Int] = [] var sum = 0 for n in numbers { sum += n result.append(sum) } result}
fn main() { let totals = runningTotals([3, 1, 4, 1, 5]) let texts = totals.map { n in "{n}" } print(texts.joined(separator: ", "))}3, 4, 8, 9, 14Summary
Section titled “Summary”- A list holds many values of one type, in order:
[72, 95, 58]is an[Int]. An empty list needs its type:var names: [String] = []. - Items are numbered from 0. Read one with
list[i], replace it withlist[i] = value. countis the number of items;isEmptytells you if there are none.append,insert(_:at:)andremove(at:)change the list, so it must be avar.- An index outside the list stops the program with a runtime error.
Check indexes against
count, or usefirstandlast, which are optional. contains,sorted(),reversed(),filter { … }andmap { … }answer questions or make new lists, leaving the original alone.- Lists are values: assigning or passing a list makes a copy.
Next: 8. Working with text