In this chapter, we’re going to see a lot of Rust’s functional style, which is based on expressions. This style lets you use a method that gives an output, that output becomes the next method’s input, and you repeat until you have the final output that you want. It works like a chain of methods, which is why people call it method chaining. Method chaining is a lot of fun once you get used to it; it lets you do a lot of work with not very much code. Iterators and closures are a big help here, so they are the main focus of this chapter.
Rust is a systems programming language like C and C++, and its code can be written as separate commands in separate lines, but it also has a functional style. Both styles are okay, but functional style is usually shorter.
Here is an example of the nonfunctional style (called imperative style) to make a Vec
from 1 to 10:
fn main() { let mut new_vec = Vec::new(); let mut counter = 1; loop { new_vec.push(counter); counter += 1; if counter == 10 { break; } } println!("{new_vec:?}"); }
This prints [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.
Imperative means to give orders or instructions, and that’s what this example shows. (Indeed, the word imperative and emperor are related: an emperor is the person who gives orders to everyone else.) The code is being instructed to do a lot of individual things: start a loop, push into a Vec
, increase a variable called counter
, check the value of counter
, and break out of the loop at a certain point.
But functional style is more about expressions: taking the output of an expression, putting that into a new function, taking that output, putting it into yet another function, and so on until finally you have the result that you want.
Here is an example of Rust’s functional style that does the same as the previous code:
fn main() {
let new_vec = (1..).take(10).collect::<Vec<i32>>(); ①
println!("{new_vec:?}");
}
① Or you can write it like this: let new_vec: Vec<i32> = (1..).take(10).collect();.
This code starts with a range (an iterator) that goes up from 1
. It has a method called .take()
, which we can use to take the first 10 items. After that, you can call another method, .collect()
, to turn it into a Vec. .collect()
can make collections of many types, so we have to tell .collect()
the type here.
With functional style, you can chain as many methods as you want. Here is an example of many methods chained together:
fn main() { let my_vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let new_vec = my_vec.into_iter().skip(3).take(4).collect::<Vec<i32>>(); println!("{new_vec:?}"); }
This creates a Vec
with [3, 4, 5, 6]
. This is a lot of information for one line, so it can help to put each method on a new line. Doing that makes it easier to read and demonstrates the functional style a lot better. Read the following code line by line and try to guess what the output of the code will be:
fn main() { let my_vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let new_vec = my_vec .into_iter() ① .skip(3) ② .take(4) ③ .collect::<Vec<i32>>( ④ println!("{new_vec:?}"); }
① Iterates over the items (iterate = work with each item inside it). into_iter() gives us owned values, not references.
② Skips over three items: 0, 1, and 2
③ Takes the next four: 3, 4, 5, and 6
The output will be a Vec
that holds the values [3, 4, 5, 6]
.
We can use this functional style best once we understand what exactly iterators and closures are. So we will learn them next.
An iterator is sort of like a collection type that gives you its items one at a time. It’s a little bit like someone dealing a deck of cards. You can take one card at a time until the deck runs out. Or you can draw the fifth card. Or you can skip 10 cards and take the next 10. Or you can ask for the 60th card and be told that there is no 60th card in the deck.
We have already used iterators a lot because the for
loop gives you an iterator. When you want to use an iterator other times, you have to choose what kind:
A for
loop is an iterator of values, so typing for item in iterator
is the same as typing for item in iterator.into_iter()
. Let’s look at a quick example of these three types of iterators:
fn main() { let vector1 = vec![1, 2, 3]; let mut vector2 = vec![10, 20, 30]; for num in vector1.iter() { ① println!("Printing a &i32: {num}"); } for num in vector1 { ② println!("Printing an i32: {num}"); } for num in vector2.iter_mut() { ③ *num *= 10; println!("num is now {num}"); } println!("{vector2:?}"); // println!("{vector1:?}"); ④ }
① First, we use .iter() so that vector1 is not destroyed.
② This is the same as writing "for num in vector1.into_iter()." It owns the values, and vector1 no longer exists after this for loop is done.
③ This for loop takes mutable references, so vector2 still exists after it is over.
④ We can still print vector2, but vector1 is gone. The compiler will give an error if you uncomment this last line.
Printing a &i32: 1 Printing a &i32: 2 Printing a &i32: 3 Printing an i32: 1 Printing an i32: 2 Printing an i32: 3 num is now 100 num is now 200 num is now 300 [100, 200, 300]
You don’t need to use for
to use an iterator, though. Here is another way to use them:
fn main() {
let vector1 = vec![1, 2, 3];
let vector1_a = vector1
.iter() ①
.map(|x| x + 1)
.collect::<Vec<i32>>();
let vector1_b = vector1
.into_iter()
.map(|x| x * 10)
.collect::<Vec<i32>>();
let mut vector2 = vec![10, 20, 30];
vector2.iter_mut().for_each(|x| *x +=100);
println!("{:?}", vector1_a);
println!("{:?}", vector1_b);
println!("{:?}", vector2);
}
① Here as well, we are using .iter() first so that vector1 is not destroyed.
[2, 3, 4] [10, 20, 30] [110, 120, 130]
For the first two, we used a method called .map()
. This method lets you do something to every item (including turning it into a different type) and then pass it on to make a new iterator. The last one we used is one called .for_each()
. This method lets you do something with every item without creating a new iterator. .iter_mut()
plus .for_each()
is basically a for
loop. Inside each method, we can give a name to every item (we called it x
) and use that to change it. These are called closures, and we will learn about them in the next section. For now, just remember that a closure uses ||
where a regular function uses ()
, so |x|
means “x
gets passed into the closure (the function).”
Let’s go over them again, one at a time. First, we used .iter()
on vector1
to get references. We added 1 to each and passed it on with .map()
. Then we collected it into a new Vec
:
let vector1_a = vector1.iter().map(|x| x + 1).collect::<Vec<i32>>();
The original vector1
is still alive because we only used references: we didn’t take by value. Now we have vector1
, and a new Vec
called vector1_a
. Because .map()
just passes it on, we needed to use .collect()
to make it into a Vec
.
Then we used .into_iter()
to get an iterator by value from vector1
:
let vector1_b = vector1.into_iter().map(|x| x * 10).collect::<Vec<i32>>();
This destroys vector1
because that’s what .into_iter()
does. Therefore, after we make vector1_b
, we can’t use vector1
again.
Finally, we used .iter_mut()
for vector2
:
let mut vector2 = vec![10, 20, 30]; vector2.iter_mut().for_each(|x| *x +=100);
It is mutable, so we don’t need to use .collect()
to create a new Vec
. Instead, we change the values in the same Vec
with mutable references. Thus, vector2
is still there after the iterator is over. We want to modify each item but don’t need to make a new Vec
, so we use the .for_each()
method.
The core of every iterator is a method called .next()
, which returns an Option
. When you use an iterator, it calls .next()
over and over again to see whether there are more items left. If .next()
returns Some
, there are still items left, and the iterator keeps going. If None
is returned, the iteration is finished. (Well, usually. You can actually make iterators that never return None
, only return None
, and so on. We’ll see some of those soon.) But generally, an iterator gives out a bunch of Some
s until it runs out, and then it only gives None
. This is how the for
loops in the previous examples knew when to stop looping. If you wish, you can also manually call .next()
on an iterator if you want more control, as the next example shows.
Do you remember the assert_eq!
macro? You see it all the time in documentation. Here it is showing how an iterator works:
fn main() { let my_vec = vec!['a', 'b', '거', '柳']; ① let mut my_vec_iter = my_vec.iter(); ② assert_eq!(my_vec_iter.next(), Some(&'a')); ③ assert_eq!(my_vec_iter.next(), Some(&'b')); assert_eq!(my_vec_iter.next(), Some(&'거')); assert_eq!(my_vec_iter.next(), Some(&'柳')); assert_eq!(my_vec_iter.next(), None); ④ assert_eq!(my_vec_iter.next(), None); ⑤ }
② The Vec is an Iterator type now, but we haven’t called .next() on it yet. It’s an iterator waiting to be called.
③ Calls the first item with .next() and then again and again. Each time the iterator will return Some with the value inside.
④ Now the iterator is out of items, so it returns None.
⑤ You can keep calling .next() on the iterator, and it will simply return None every time.
The previous code will output absolutely nothing! That’s because the output of the iterator matched our assertion, and so nothing happened (the code didn’t panic). This is an interesting, indirect way to show what is happening in code without printing.
Implementing Iterator
for your own types is not too difficult. First, let’s make a book library struct and think about how we might want to use an iterator there. The code is pretty simple:
#[derive(Debug)] struct Library { name: String, books: Vec<String>, } impl Library { fn add_book(&mut self, book: &str) { self.books.push(book.to_string()); } fn new(name: &str) -> Self { Self { name: name.to_string(), books: Vec::new(), } } } fn main() { let my_library = Library::new("Calgary"); println!("{my_library:?}"); }
So far, it just prints Library { name: "Calgary", books: [] }
. It’s a new library, empty and ready to put some books in. When you add books to the library, they will be inside a Vec<String>
. You can turn a Vec
into an iterator whenever you want, as we just saw. But what if you want to change the behavior a bit? Then you can implement Iterator
for your own type. Let’s look at the Iterator
trait in the standard library (https://doc.rust-lang.org/std/iter/trait.Iterator.html) and see whether we can figure it out.
The top left of the page has the most important information we need to know:
An associated type means “a type that goes together” (it goes together with the trait). Returning a String
sounds like a good idea for our iterator, so we will choose String
for the association type. And we want to implement it on this type of our own because of the orphan rule we learned in the last chapter. You can’t implement Iterator
for Vec<String>
because we didn’t create the Vec
type, and we didn’t create the String
type. But we can put a Vec<String>
inside our own type, and now we can implement traits on it.
First, we’ll change our Library
a bit. The books are now a struct called BookCollection
, which holds a Vec<String>
. And we’ll add a method that clones it so we can do what we want with it without touching the original Library
. Now it looks like this:
#[derive(Debug)]
struct Library {
name: String,
books: BookCollection,
}
#[derive(Debug, Clone)]
struct BookCollection(Vec<String>); ①
impl Library {
fn add_book(&mut self, book: &str) {
self.books.0.push(book.to_string());
}
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
books: BookCollection(Vec::new()),
}
}
fn get_books(&self) -> BookCollection {
self.books.clone()
}
}
① BookCollection is just a Vec<String> on the inside, but it’s our type, so we can implement traits on it.
How do we implement Iterator
on this BookCollection
type? Let’s look at the page in the standard library on the Iterator
trait again. That page has a simple example of an iterator that looks like this:
struct Alternate { ① state: i32, } impl Iterator for Alternate { type Item = i32; fn next(&mut self) -> Option<i32> { let val = self.state; self.state = self.state + 1; if val % 2 == 0 { ② Some(val) } else { None } } }
① An iterator which alternates between Some and None
② If it’s even, Some(i32), else None
You can see that under impl Iterator for Alternate
it says type Item = i32
. This is the associated type. Our iterator will be for our list of books, which is a BookCollection
. When we call .next()
, it will give us a String
. We will copy this code except we will use type Item = String;
. That is our associated item.
To implement Iterator
, you also need to write the .next()
method. This is where you decide what the iterator should do. For BookCollection
in our Library
, we will do something simple: give us the last books first. We will also imagine that we want to print the output every time an item is found so that we can log it somewhere, so we will stick a println!
inside the .next()
method to log this information. (Maybe the library council wants to keep track of what each library is doing.) So we will match
with .pop()
, which takes the last item off if it is Some
. Now it looks like this:
#[derive(Debug)] struct Library { name: String, books: BookCollection, } #[derive(Clone, Debug)] struct BookCollection(Vec<String>); impl Library { fn add_book(&mut self, book: &str) { self.books.0.push(book.to_string()); } fn new(name: &str) -> Self { Self { name: name.to_string(), books: BookCollection(Vec::new()), } } fn get_books(&self) -> BookCollection { self.books.clone() } } impl Iterator for BookCollection { type Item = String; fn next(&mut self) -> Option<String> { match self.0.pop() { Some(book) => { println!("Accessing book: {book}"); Some(book) } None => { println!("Out of books at the library!"); None } } } } fn main() { let mut my_library = Library::new("Calgary"); my_library.add_book("The Doom of the Darksword"); my_library.add_book("Demian - die Geschichte einer Jugend"); my_library.add_book("구운몽"); my_library.add_book("吾輩は猫である"); for item in my_library.get_books() { println!("{item}"); } }
Accessing book: 吾輩は猫である 吾輩は猫である Accessing book: 구운몽 구운몽 Accessing book: Demian - die Geschichte einer Jugend Demian - die Geschichte einer Jugend Accessing book: The Doom of the Darksword The Doom of the Darksword Out of books at the library!
You can see that .next()
did indeed return None
once because we told the code to print out Out of books at the library!
if None
is returned from the function.
In this example, we just popped off each item and printed it out before we passed it off as Some
, but you can implement an iterator in very different ways. You don’t ever need to return None
if you want an iterator that never ends. Here’s an iterator that just gives the number 1 forever:
struct GivesOne; impl Iterator for GivesOne { type Item = i32; fn next(&mut self) -> Option<i32> { Some(1) } }
If you use a while
loop that continues as long as the iterator returns Some
, the program will never stop. But you can use the .take()
method we learned before to only call it five times and then collect that into a Vec
:
struct GivesOne; impl Iterator for GivesOne { type Item = i32; fn next(&mut self) -> Option<i32> { Some(1) } } fn main() { let five_ones: Vec<i32> = GivesOne.into_iter().take(5).collect(); println!("{five_ones:?}"); }
This prints out [1, 1, 1, 1, 1]
.
Note that the GivesOne
struct doesn’t hold anything! It’s a good example of one of the ways that an iterator differs from a collection type. In this case, the GivesOne
struct is just an empty struct that implements the Iterator
trait.
There is quite a bit more to know about iterators, but now we understand the basics. You see closures a lot when using the iterators in Rust’s standard library, so let’s learn about them now.
Closures are quick functions that don’t need a name—in other words, anonymous functions. Sometimes they are called lambdas in other languages. It’s easy to notice where closures are because they use ||
instead of ()
. They are very common in Rust, and once you learn to use them, you will wonder how you lived without them.
You can bind a closure to a variable, and then it looks exactly like a function when you use it:
fn main() { let my_closure = || println!("This is a closure"); my_closure(); }
This closure takes nothing: ||
and prints a message: This is a closure
.
In between the ||
, we can add input variables and types in the same way that we put them inside ()
for regular functions. This next closure takes an i32
and prints it out:
fn main() { let my_closure = |x: i32| println!("{x}"); my_closure(5); my_closure(5+5); }
5 10
For longer closures, you need to add a code block. Then it can be as long as you want:
fn main() {
let my_closure = || {
let number = 7;
let other_number = 10;
println!("The two numbers are {number} and {other_number}.");
};
my_closure(); ①
}
① This closure can be as long as we want, just like a function.
One thing that makes closures special is that they can take variables from their environment that are outside the closure, even if you only write ||
. You can think of a closure as a standalone type that can hold references in the same way that a struct can.
NOTE If you’re curious about the details of closure types, see the page in the reference here: https://doc.rust-lang.org/reference/types/closure.html.
fn main() { let number_one = 6; let number_two = 10; let my_closure = || println!("{}", number_one + number_two); my_closure(); }
Calling the closure my_closure
prints 16
. You didn’t need to put anything in ||
because it can just take number_one
and number_two
and add them. If you want to be very correct:
A ||
that doesn’t enclose a variable from outside is an anonymous function. Anonymous means “doesn’t have a name.” It works more like a regular function and can be passed into places where a function is required if the signature is the same.
A ||
that encloses a variable from outside is also anonymous but called a closure. It “encloses” the variables around it to use them.
But people will often call all ||
functions closures, so you don’t have to worry about the name too much. We will call anything with a ||
a closure, but remember that it can mean an anonymous function.
Let’s look at some more things that closures can do. You can do this:
fn main() { let number_one = 6; let number_two = 10; let my_closure = |x: i32| println!("{}", number_one + number_two + x); my_closure(5); }
This closure takes number_one
and number_two
. We also gave it the new variable x
and said that x
is 5
. Then it adds all three together to print 21
.
Usually you see closures in Rust inside of methods because it is very convenient to have a closure inside. The convenience comes from the fact that the user can write the body of the closure differently each time, depending on the situation. We saw closures in the last section with .map()
and .for_each()
. The closure inside .for_each()
, for example, simply takes a mutable reference to the item and returns nothing, and with that freedom, the user of the .for_each()
method can do anything inside as long as the signature matches. Here is a quick example:
fn main() { (1..=3).for_each(|num| println!("{num}")); (1..=3).for_each(|num| { println!("Got a {num}!"); if num % 2 == 0 { println!("It's even") } else { println!("It's odd") }; }); }
1 2 3 Got a 1! It's odd Got a 2! It's even Got a 3! It's odd
Here is another example: remember the .unwrap_or()
method that we learned that you can use to return a default value if an Option
is a None
or Result
is an Err
? The following code will print 0
instead of panicking because we gave it the default value 0
:
fn main() { let nothing: Option<i32> = None; println!("{}", nothing.unwrap_or(0)); }
There is another similar method called .unwrap_or_else().
This method also allows us to give a default value, except that it passes on a closure that we can use to write some more complex logic. See whether you can guess what the output for this code sample will be:
fn main() { let my_vec = vec![8, 9, 10]; let fourth = my_vec.get(3).unwrap_or_else(|| { ① if let Some(val) = my_vec.get(2) { ② val } else { ③ &0 } }); println!("{fourth}"); }
① First, we try to get an item at index 3.
② If it doesn’t work, maybe we have a good reason to look for an item one index back. Inside the closure we can try .get() again! And then return that value if it’s found at index 2.
③ And then finally we will return a &0 in case no items have been found at either index.
The output is 10
because there was no item at index 3, but an item was then found at index 0, which was a 10
.
A closure can, of course, be very simple. For example, you can write let fourth = my_vec.get(3).unwrap_or_else(|| &0);
. You don’t always need to use a {}
and write complicated code just because there is a closure.
There are a lot of methods for iterators that enhance the iterator in a certain way. For example, let’s say that you have an iterator that holds the char
s 'z', 'y'
, and 'x'
. An iterator will return Some('z')
, then Some('y')
, and finally Some('x')
before starting to return None
. But what if you wanted to also see the index of each item along with the item itself?
Well, it turns out that all you have to do is add .enumerate()
to an iterator to make this happen. (You might see this called zip with index in other languages.)
fn main() { let char_vec = vec!['z', 'y', 'x']; char_vec .iter() ① .enumerate() ② .for_each(|(index, c)| println!("Index {index} is: {c}")); }
① Makes char_vec into an iterator
② Now, each item is (usize, char) instead of just char.
Index 0 is: 'z' Index 1 is: 'y' Index 2 is: 'x'
In this case, we use .for_each()
instead of .map()
because we didn’t need to collect char_vec
into a new iterator.
Meanwhile, .map()
is for doing something to each item and passing it on, as we previously saw.
One interesting thing about .map()
is that it doesn’t do anything unless you use a method like .collect()
. Let’s take a look at .map()
again, first with collect
. Here is a classic example of using .map()
to make a new Vec
from an existing Vec
:
fn main() { let num_vec = vec![2, 4, 6]; let double_vec: Vec<i32> = num_vec ① .iter() ② .map(|num| num * 2) ③ .collect(); ④ println!("{:?}", double_vec); }
③ Multiplies each item by 2 and passes it on
That was pretty easy and prints [4, 8, 12]
. But let’s see what happens when we don’t collect into a Vec
. The code won’t panic, but the compiler will tell you that you didn’t do anything:
fn main() { let num_vec = vec![2, 4, 6]; num_vec .iter() .enumerate() .map(|(index, num)| format!("Index {index} is {num}")); }
warning: unused `Map` that must be used --> src/main.rs:4:5 | 4 | / num_vec 5 | | .iter() 6 | | .enumerate() 7 | | .map(|(index, num)| format!("Index {index} is {num}")); | |______________________________________________________________^ | = note: iterators are lazy and do nothing unless consumed
This is a warning, so it’s not an error: the program runs fine. But why doesn’t num_vec
do anything? We can look at the types to see:
.iter()
—Now it is an Iter<i32>
, so it is an iterator with items of i32
.
.enumerate()
—Now it is an Enumerate<Iter<i32>>
, so it is a type Enumerate
of type Iter
of i32
s.
.map()
—Now it is a type Map<Enumerate<Iter<i32>>>
, so it is a type Map
of type Enumerate
of type Iter
of i32
s.
All we did was make a more and more complicated structure. So this Map<Enumerate <Iter<i32>>>
is a structure that is ready to go, but only when we tell it what to do. This is one of the ways that Rust keeps even fancy functional-looking code as fast as any other kind of code. Rust avoids this sort of operation:
Instead, an iterator with a method and another method and another method simply creates a single structure and waits until we decide what to do with it. If we add .collect::<Vec<i32>>()
, it knows what to do. This is what iterators are lazy and do nothing unless consumed
means. The iterators don’t do anything until you “consume” them (use them up).
This is an example of an idea in Rust called zero-cost abstractions. The idea behind zero-cost abstractions is that complicated code might or might not take longer to compile, but at run time, they will be the same speed. Your program won’t be any slower if you use a complicated iterator than if you wrote everything by hand.
You can even create complicated things like HashMap
using .collect()
, so it is very powerful. Here is an example of how to put two Vec
s into a HashMap
. First, we make two vectors, one for the keys and the other for the values. We will then use .into_iter()
on each of them to get an iterator of values. Then we use the .zip()
method. This method takes two iterators and attaches them together, like a zipper. Finally, we use .collect()
to make the HashMap
. Here is the code:
use std::collections::HashMap; fn main() { let some_keys = vec![0, 1, 2, 3, 4, 5]; let some_values = vec!["zero", "one", "two", "three", "four", "five"]; let number_word_hashmap = some_keys .into_iter() ① .zip(some_values.into_iter()) ② .collect::<HashMap<_, _>>(); println!( "The value at key 2 is: {}", number_word_hashmap.get(&2).unwrap() ); }
② On this line, .zip() takes our iterator and zips it together with the second.
This prints The value at key 2 is: two
.
You can see that we wrote <HashMap<_, _>>
because that is enough information for Rust to decide on the type HashMap<i32, &str>
. You can write .collect::<HashMap <i32, &str>>();
if you want, or you can declare the type up front like this if you prefer:
use std::collections::HashMap; fn main() { let some_numbers = vec![0, 1, 2, 3, 4, 5]; let some_words = vec!["zero", "one", "two", "three", "four", "five"]; let number_word_hashmap: HashMap<_, _> = some_numbers ① .into_iter() .zip(some_words.into_iter()) .collect(); ② }
① We specified the type here ...
② ... so we don’t have to type anything after .collect() here: Rust already knows the type to collect into.
Or you can turn the Vec
s into iterators right away! This code also does the exact same thing as the previous two samples:
use std::collections::HashMap; fn main() { let keys = vec![0, 1, 2, 3, 4, 5].into_iter(); let values = vec!["zero", "one", "two", "three", "four", ➥"five"].into_iter(); ① let number_word_hashmap: HashMap<i32, &str> = keys.zip(values).collect(); println!( "The value at key 2 is: {}", number_word_hashmap.get(&2).unwrap() ); }
① Both some_keys and some_values are now iterators. Now, the first lines are a bit longer but the .zip() method looks cleaner.
There is another method that is like .enumerate()
for char
: .char_indices()
. (Indices means “indexes.”) You use it in the same way. Let’s take a big string of numbers and print them three characters at a time with a tab-length space between them:
fn main() { let numbers_together = "140399923481800622623218009598281"; for (index, num) in numbers_together.char_indices() { match (index % 3, num) { ① (0 | 1, num) => print!("{num}"), ② _ => print!("{num}\t"), ③ } } }
① We’ll use the index number modulo 3 to get the remainder after dividing by 3.
② You can also use | in match statements, meaning "or"—in this case, 0 or 1.
③ The only other possible remainder after dividing by 3 is 2, but Rust doesn’t know that, so we’ll use a _ wildcard.
140 399 923 481 800 622 623 218 009 598
➥281
Sometimes you see |_|
in a closure. It’s not a special syntax, though. It only means that the closure needs to take an argument that you give a name to (like x
or num
), but you don’t want to use it. |_|
means “okay, this closure takes an argument, but I won’t give it a name because I won’t bother to use it.”
Here is an example of an error when you don’t do that:
fn main() { let my_vec = vec![8, 9, 10]; my_vec .iter() .for_each(|| println!("We didn't use the variables at all")); }
error[E0593]: closure is expected to take 1 argument, but it takes 0 arguments --> src/main.rs:5:10 | 5 | .for_each(|| println!("We didn't use the variables at all")); | ^^^^^^^^ -- takes 0 arguments | | | expected closure that takes 1 argument
It then continues with some pretty good advice:
help: consider changing the closure to take and ignore the expected argument | 5 | .for_each(|_| println!("We didn't use the variables at all")); | ~~~
Sounds good. If you change ||
to |_|
, it will work because it takes the argument but then ignores it.
That should be enough for an introduction to iterators and closures. Hopefully, you enjoyed learning about what they are and how they work because they are everywhere in Rust and very convenient. We only took a quick look in this chapter, and there’s a lot more to learn. If you don’t feel like you understand iterators and closures yet, don’t worry—we aren’t changing subjects yet. The next chapter is also about the same thing! In chapter 9, we will take a look at some of the most common methods for iterators and closures.
Method chaining can be unfamiliar at first, but it is so convenient that people tend to use it more and more as they become familiar with Rust.
The core method in iterators is .next()
, which returns an Option
. Almost all iterators return Some
until they run out of items, and after that, None
.
Iterators are lazy. To use one, call .next()
or use a method like .collect()
to turn it into another type (usually a Vec
).
You can give a closure a name if you want to use the name to call the closure later. But most of the time, you don’t give names to closures.
A closure can capture variables in its scope. You don’t need to pass the variables in as arguments—the closure can just grab them.
An associated type is the type that goes with a trait. Most traits don’t have them, but some do.
You, as the user, decide what the concrete type of an associated type will be when you implement a trait.