11. Structs
So far every value you’ve used has been one thing: a number, a piece of
text, a list. But the things programs deal with usually have several parts.
A book has a title, an author and a number of pages. A point on a map has an
x and a y. In this lesson you’ll learn to make your own types that keep
those parts together.
In this lesson you’ll learn:
- how to declare a struct, a type made of named parts called fields
- how to create struct values, and give fields default values
- how to read and change fields
- how to write methods, functions that belong to a struct
- why a struct is a value, and what that means when you copy it
- how to keep many structs in a list
The problem: values that belong together
Section titled “The problem: values that belong together”Say you want to keep track of some books. With what you know so far, you might write:
let title = "The Hobbit"let author = "J. R. R. Tolkien"let pages = 310let title2 = "Dune"let author2 = "Frank Herbert"let pages2 = 412This gets messy fast. Nothing says that title2 and pages2 belong
together, and passing “a book” to a function means passing three separate
values. What you want is one value that is a book.
Declaring a struct
Section titled “Declaring a struct”A struct (short for “structure”) is a new type that you describe yourself. You list its parts, called fields, each with a name and a type:
struct Book { title: String author: String pages: Int}
fn main() { let book = Book(title: "The Hobbit", author: "J. R. R. Tolkien", pages: 310) print(book.title) print("{book.title} has {book.pages} pages")}The HobbitThe Hobbit has 310 pagesA few things to notice:
- The struct is declared outside
main, at the top level of the file. NowBookis a type, just likeIntorString. - Type names start with a capital letter by convention:
Book,Point,Player. Variables start with a small letter:book. - To create a book, you call
Book(...)like a function, with a label for each field. - To read a field, write the value, a dot, and the field name:
book.title.
Creating values
Section titled “Creating values”When you create a struct value, you give the fields in the order they are declared. The labels are there so you can see what each value means, and so Tessel can catch mixups. Put them in the wrong order and you get an error:
let book = Book(author: "Frank Herbert", title: "Dune", pages: 412)error: `title:` is in the wrong position --> main.tsl:8:46 |8 | let book = Book(author: "Frank Herbert", title: "Dune", pages: 412) | ^^^^^ | = help: arguments go in this order: `title:`, `author:`, `pages:`Leave one out and Tessel tells you which:
error: `Book` is missing `author:`Default values
Section titled “Default values”Some fields have an obvious starting value. A new book usually hasn’t been
read yet. Give a field a default value with =, and you can leave it out
when you create the value:
struct Book { title: String author: String pages: Int read: Bool = false rating: Int? = nil}
fn main() { let hobbit = Book(title: "The Hobbit", author: "J. R. R. Tolkien", pages: 310) let dune = Book(title: "Dune", author: "Frank Herbert", pages: 412, read: true) print(hobbit.read) print(dune.read) print(dune.rating ?? 0)}falsetrue0rating is an optional (Int?), because not every book has a rating yet.
An optional field still needs a value, so give it the default nil if it
should start out empty.
Changing fields
Section titled “Changing fields”To change a field, assign to it with =, or use += and friends. The value
must be stored in a var:
struct Point { x: Int y: Int}
fn main() { var p = Point(x: 2, y: 3) p.x = 10 p.y += 1 print("({p.x}, {p.y})")}(10, 4)Common mistake: changing a field of a let
Section titled “Common mistake: changing a field of a let”A let constant can never change, and that includes its fields. If you
write let p = Point(x: 2, y: 3) and then p.x = 10, Tessel stops you:
error: can't change `p`, which is a constant --> main.tsl:8:5 |8 | p.x = 10 | ^^^ ::: main.tsl:7:9 |7 | let p = Point(x: 2, y: 3) | - declared here | = help: declare it with `var` to be able to change itThe fix is what the help line says: use var p. This is on purpose. When
you see let, you know the whole value stays the same, every field of it.
Printing a struct
Section titled “Printing a struct”You can’t print a whole struct directly. print(p) gives:
error: `Point` can't be shown as textTessel doesn’t guess how you’d like a point to look. Print the fields you want instead, or write a method that describes the value, as you’ll do next.
Methods
Section titled “Methods”A method is a function that lives inside a struct. You call it on a value with a dot, the same way you read a field:
struct Point { x: Int y: Int
fn describe() -> String { "({x}, {y})" }
fn distanceFromOrigin() -> Int { abs(x) + abs(y) }}
fn main() { let p = Point(x: 3, y: -4) print(p.describe()) print(p.distanceFromOrigin())}(3, -4)7Inside a method, you can use the fields by name: x and y mean the x
and y of the point the method was called on. So p.describe() uses p’s
fields. Methods can take parameters and return results like any function.
Why put a function inside the struct instead of writing
fn describe(point: Point) outside? Because the method belongs to points.
Anyone reading p.describe() knows where to look for it, and your editor can
suggest it when you type p..
Methods that change the value
Section titled “Methods that change the value”A method can also change the struct’s fields:
struct Point { x: Int y: Int
fn describe() -> String { "({x}, {y})" }
fn move(dx: Int, dy: Int) { x += dx y += dy }}
fn main() { var p = Point(x: 0, y: 0) p.move(dx: 2, dy: 5) p.move(dx: 1, dy: -1) print(p.describe())}(3, 4)You don’t have to mark move in any special way. Tessel sees that it
assigns to a field and remembers that move changes the point. That has
one consequence: you can only call it on a value that is allowed to change,
like a var. Call it on a let and you get:
error: can't call `move` on `p`, which is a constant, because `move` changes it --> main.tsl:13:5 |13 | p.move(dx: 2, dy: 5) | ^ ::: main.tsl:12:9 |12 | let p = Point(x: 0, y: 0) | - declared here | = help: declare it with `var` to be able to change itInside a method, the whole value is called self. You rarely need it,
because fields are available by name. The one time you do is when a
parameter has the same name as a field:
struct Player { name: String score: Int = 0
fn rename(name: String) { self.name = name }}
fn main() { var player = Player(name: "ada") player.rename(name: "Ada") print(player.name)}AdaHere name alone means the parameter, and self.name means the field.
Structs are values
Section titled “Structs are values”In Tessel, a struct behaves like a number. When you assign it to another variable, you get a separate copy. Changing the copy leaves the original alone:
struct Point { x: Int y: Int}
fn main() { var a = Point(x: 1, y: 1) var b = a b.x = 50 print("a is ({a.x}, {a.y})") print("b is ({b.x}, {b.y})")}a is (1, 1)b is (50, 1)This is the same thing that happens with var b = a when a is an Int:
changing b never changes a. It means a value can only change through the
variable that holds it, so you never get surprised by something changing
“behind your back”. (If you’ve used Python or JavaScript, be careful: there,
b = a would make two names for the same object. Not in Tessel.)
The same goes for functions. A function gets its own copy of each argument, and it can’t change its parameters at all:
fn moveRight(point: Point) { point.x += 1}error: can't change `point`, which is a parameter --> main.tsl:7:5 |7 | point.x += 1 | ^^^^^^^ ::: main.tsl:6:14 |6 | fn moveRight(point: Point) { | ----- declared here | = help: parameters are copies and can't be changed; copy it into a `var`, or (in a view) declare it as `bind`Instead, have the function make a changed copy and return it. (Or use a
method that changes the value, like move above.)
struct Point { x: Int y: Int}
fn movedRight(point: Point) -> Point { var moved = point moved.x += 1 moved}
fn main() { let start = Point(x: 1, y: 1) let next = movedRight(point: start) print("start: {start.x}, next: {next.x}") print(start == next) print(start == Point(x: 1, y: 1))}start: 1, next: 2falsetrueThe last two lines show that == works on structs: two values are equal
when all their fields are equal.
Lists of structs
Section titled “Lists of structs”A struct is a type, so you can have a list of them, [Todo], just like a
list of Ints:
struct Todo { title: String done: Bool = false}
fn main() { var todos = [Todo(title: "Buy milk"), Todo(title: "Call Sam"), Todo(title: "Water plants")] todos[1].done = true todos.append(Todo(title: "Read a book"))
for todo in todos { let mark = if todo.done { "x" } else { " " } print("[{mark}] {todo.title}") }}[ ] Buy milk[x] Call Sam[ ] Water plants[ ] Read a booktodos[1].done = true changes the field of the item inside the list. That
works because the whole path, todos[1].done, starts at a var.
Common mistake: changing a copy
Section titled “Common mistake: changing a copy”Because structs are values, taking an item out of a list gives you a copy:
struct Todo { title: String done: Bool = false}
fn main() { var todos = [Todo(title: "Buy milk"), Todo(title: "Call Sam")] var first = todos[0] first.done = true print(todos[0].done) todos[0].done = true print(todos[0].done)}falsetrueSetting first.done only changed the copy in first. To change the item in
the list, change it through the list: todos[0].done = true.
There’s one handy exception. When you loop over a list stored in a var,
the loop variable is the item in the list, not a copy. So inside
for todo in todos { … } you can write todo.done = true and the list
changes. You’ll use this in the next example.
Tuples: a quick group without a name
Section titled “Tuples: a quick group without a name”Sometimes a few values belong together only for a moment, like the two
answers of a division. Declaring a struct for that is more than you need. A
tuple groups values without a declaration: write them in parentheses,
(3, 2), and write its type the same way, (Int, Int). Its items are
numbered from 0: .0, .1, and so on.
fn divide(_ a: Int, by: Int) -> (Int, Int) { (a / by, a % by)}
fn main() { let result = divide(17, by: 5) print("17 / 5 is {result.0}, remainder {result.1}")
let (whole, rest) = divide(20, by: 6) print("{whole} and {rest} left over")}17 / 5 is 3, remainder 23 and 2 left overlet (whole, rest) = … takes the tuple apart into two names. A for loop
can do the same with each item of a list of tuples:
fn main() { let moves: [(String, Int)] = [("up", 3), ("right", 2), ("up", 1)] for (direction, steps) in moves { print("{direction} {steps}") }}up 3right 2up 1print(("up", 3)) shows a tuple as you’d write it, ("up", 3). And you’ve
already met tuples without their name: for (i, day) in days.enumerated()
in lesson 7 and for (item, price) in prices in lesson 9 take tuples apart.
zip(names, scores) makes a list of pairs from two lists.
Tuples are handy for returning two or three values, or for pairs in a list.
When the values deserve names (result.0 doesn’t say what it is), or you
pass them around a lot, a struct is clearer: book.title beats book.0.
Worked example: a small library
Section titled “Worked example: a small library”Let’s put it together. A Library is a struct too: it holds a list of
books, and its methods do the work of adding, searching and counting.
struct Book { title: String author: String pages: Int read: Bool = false
fn describe() -> String { "{title} by {author} ({pages} pages)" }}
struct Library { books: [Book] = []
fn add(book: Book) { books.append(book) }
fn byAuthor(author: String) -> [Book] { var found: [Book] = [] for book in books { if book.author == author { found.append(book) } } found }
fn totalPages() -> Int { var total = 0 for book in books { total += book.pages } total }
fn markRead(title: String) { for book in books { if book.title == title { book.read = true } } }
fn unreadCount() -> Int { var count = 0 for book in books { if !book.read { count += 1 } } count }}
fn main() { var library = Library() library.add(book: Book(title: "The Hobbit", author: "J. R. R. Tolkien", pages: 310)) library.add(book: Book(title: "Dune", author: "Frank Herbert", pages: 412)) library.add(book: Book(title: "The Silmarillion", author: "J. R. R. Tolkien", pages: 365))
print("Books by Tolkien:") for book in library.byAuthor(author: "J. R. R. Tolkien") { print("- {book.describe()}") }
print("Total pages: {library.totalPages()}")
library.markRead(title: "Dune") print("Still to read: {library.unreadCount()} of {library.books.count}")}Books by Tolkien:- The Hobbit by J. R. R. Tolkien (310 pages)- The Silmarillion by J. R. R. Tolkien (365 pages)Total pages: 1087Still to read: 2 of 3Some things worth noticing:
Library()needs no arguments, because its only field has a default (an empty list).addandmarkReadchange the library, solibrarymust be avar.byAuthor,totalPagesandunreadCountonly read, so they would work on alettoo.- In
markRead,book.read = truechanges the book insidebooks, because the loop runs over a field of a value that can change. mainstays short and readable. The details of how to find books by an author live in one place, insideLibrary.
Exercises
Section titled “Exercises”1. Rectangle. Make a struct Rectangle with width and height
(both Int). Give it two methods: area() and perimeter() (the length
all the way around, 2 * (width + height)). Create a 3 by 4 rectangle and
print both.
Solution
struct Rectangle { width: Int height: Int
fn area() -> Int { width * height }
fn perimeter() -> Int { 2 * (width + height) }}
fn main() { let r = Rectangle(width: 3, height: 4) print("Area: {r.area()}") print("Perimeter: {r.perimeter()}")}Area: 12Perimeter: 142. Bank account. Make a struct Account with an owner and a balance
that starts at 0. Add a method deposit(amount:) that adds money, and
withdraw(amount:) that takes money out and returns true, or returns
false and changes nothing if there isn’t enough. Deposit 100, withdraw 30,
then try to withdraw 500.
Solution
struct Account { owner: String balance: Int = 0
fn deposit(amount: Int) { balance += amount }
fn withdraw(amount: Int) -> Bool { if amount > balance { return false } balance -= amount true }}
fn main() { var account = Account(owner: "Sam") account.deposit(amount: 100) print(account.withdraw(amount: 30)) print(account.withdraw(amount: 500)) print("{account.owner} has {account.balance}")}truefalseSam has 70withdraw changes balance, so it’s a changing method, even though it also
returns a value.
3. Best student. Make a struct Student with a name and a list of
scores: [Int]. Give it a method average() -> Float. Make a list of three
students, print each one’s average with one decimal, and then print the name
of the student with the best average.
Hint: Float(n) turns an Int into a Float, and
x.formatted(decimals: 1) turns a Float into text with one decimal.
Solution
struct Student { name: String scores: [Int]
fn average() -> Float { if scores.isEmpty { return 0.0 } Float(scores.sum()) / Float(scores.count) }}
fn main() { let students = [ Student(name: "Ana", scores: [80, 92, 75]), Student(name: "Ben", scores: [95, 88, 90]), Student(name: "Cleo", scores: [70, 85, 99]), ]
var best = students[0] for student in students { print("{student.name}: {student.average().formatted(decimals: 1)}") if student.average() > best.average() { best = student } } print("Best: {best.name}")}Ana: 82.3Ben: 91.0Cleo: 84.7Best: BenThe check for an empty list matters: without it, a student with no scores would divide by zero.
4. The longest book. Add a method longest() -> Book? to the Library
from the worked example. It returns the book with the most pages, or nil
if the library is empty. Test it on an empty library and on a full one.
Solution
struct Book { title: String author: String pages: Int}
struct Library { books: [Book] = []
fn add(book: Book) { books.append(book) }
fn longest() -> Book? { var best: Book? = nil for book in books { if book.pages > (best?.pages ?? 0) { best = book } } best }}
fn main() { var library = Library() if let book = library.longest() { print(book.title) } else { print("The library is empty") }
library.add(book: Book(title: "The Hobbit", author: "J. R. R. Tolkien", pages: 310)) library.add(book: Book(title: "Dune", author: "Frank Herbert", pages: 412)) library.add(book: Book(title: "Matilda", author: "Roald Dahl", pages: 240))
if let book = library.longest() { print("Longest: {book.title} ({book.pages} pages)") } else { print("The library is empty") }}The library is emptyLongest: Dune (412 pages)best?.pages is the pages of best, or nil if there is no best book yet;
?? 0 turns that nil into 0, so the first book always wins the first
comparison.
Summary
Section titled “Summary”- A struct groups related values into one new type:
struct Book { title: String … }. - Create a value by calling the type with a label for each field, in order:
Book(title: "Dune", author: "Frank Herbert", pages: 412). - Fields can have default values (
read: Bool = false), which you can leave out when creating a value. - Read a field with a dot,
book.title. To change one, the value must be in avar. - Methods are functions inside a struct. They use the fields by name.
A method that changes fields can only be called on a
var. - Structs are values: assigning or passing one makes a copy, and
==compares all the fields. - Lists of structs work like any list. Change an item in place through the
list,
todos[0].done = true, not through a copy. - A tuple groups a few values without a declaration:
(17, "Ada")is an(Int, String). Read its items with.0,.1, or take it apart withlet (a, b) = pair.
For the full details, see Structs and enums and Values and copying.
Next: 12. Enums and match