Tutorials on Iterators

Learn about Iterators from fellow newline community members!

  • React
  • Angular
  • Vue
  • Svelte
  • NextJS
  • Redux
  • Apollo
  • Storybook
  • D3
  • Testing Library
  • JavaScript
  • TypeScript
  • Node.js
  • Deno
  • Rust
  • Python
  • GraphQL
  • React
  • Angular
  • Vue
  • Svelte
  • NextJS
  • Redux
  • Apollo
  • Storybook
  • D3
  • Testing Library
  • JavaScript
  • TypeScript
  • Node.js
  • Deno
  • Rust
  • Python
  • GraphQL

Rust Iterators: A Guide

By the time you walk out of here, you should understand what iterators are good for, how they work internally, and how to create your own!Iterators are a fairly central concept to Rust. If you're looping over something, you're very likely already using an iterator. If you're transforming collections, you probably should be using them. If your function returns a lazily evaluated sequence of things, you should consider returning an iterator - especially if that sequence could be lazily evaluated. We'll take a look at how iterators are implemented, how to iterate over a collection, what sorts of iterators exist in the standard library, usage and common patterns for transforming data, and finally a few examples of useful crates that provide their functionality via powerful iterators. Skill-wise, you'll ideally have an understanding of I highly recommend having some sort of environment to run snippets of code in. The simplest thing to use is the Rust Playground . If you'd like a local environment, refer to The Book for guidance on setting that up. Essentially, an iterator is a thing that allows you to traverse some sort of a sequence. Note that since Rust's iterators are lazy, this sequence could be generated on the fly - you could just as well traverse an existing array of finite length or create an iterator that keeps spewing out random numbers infinitely. In Rust, iterators are typically implemented using the Iterator trait. All we need to implement that trait for our custom type is provide an associated type Item (this is the type of the elements of the sequence, returned by the iterator) and the next method. Let's try and implement an iterator over numbers from 1 to 10. The next method is expected to yield the next element of the sequence. It takes a mutable reference to self in case we need to keep track of some state between next calls, which is normally the case. We'll soon find it useful. The return type of next is Option<Self::Item> . If an iterator is finite, it needs to return None to indicate it has no more elements to return. Right now, this iterator will immediately finish, not yielding any items. Let's fix this. This should be pretty self-explanatory. Every call to next increments self.current and yields it until it grows beyond 10. So we have an iterator. Let's use it. The most basic thing we can do is simply loop over all of its elements: There's actually an Iterator implementation for the Range type in Rust! A Range is what you get when you type something like 1..5 . Instead of writing the above custom iterator, we could have simply done this: Sometimes you'll want to provide the user of your code the ability to iterate over your type, but without that type itself being an iterator. This will be the case with collections. If you implement your own vector type, you probably don't want that type to needlessly hold an extra iteration variable just in case someone wants to iterate over it. What we want is a way to create an iterator out of the collection. There are three mechanisms you'll typically see. If we want to iterate over a vector of chars, we could do something like this: If we leave out the into_iter() call, Rust will call that method implicitly anyway. This implicit call is important to keep in mind. Since into_iter() consumes the data, we cannot use the original vector later. One solution would be to explicitly call v.iter() so that the iterator is borrowing instead. Another is to provide a reference to v rather than the owned value. This way, the compiler can no longer implicitly call into_iter() since it doesn't get an owned value. It gets an immutable reference, so the best it can do is implicitly call iter() on it - and that's what we want. Then there's mutability. Following the same pattern, here Rust will implicitly call iter_mut() and give us an iterator that is mutably borrowing. If all you could do with iterators was loop over them with the for keyword, they wouldn't be all that useful. But there is a plethora of adapters that transform an iterator into another kind of iterator, altering its behavior. Most adapters you'll work with are in the standard library and exist as methods provided for the implementers of the Iterator trait . There are some crates out there (such as itertools ) that provide extra adapters via extensions. We're going to go through a few useful adapters, but I highly recommend taking a look at the full list in Rust documentation . We can construct an infinite iterator using std::iter::repeat . It would be a bad idea to iterate over it directly. If we wanted to print only a few 1s, however, we can use the take adapter for that. What take does under a hood is wrap the Repeat iterator in a Take wrapper. Take is also an iterator, but this one finishes after returning a set number of elements. A very typical feature of functional programming is the ability to map , that is to apply a function to every element of a sequence. If we wanted to find only elements that fulfill a certain criterion, we can use the filter adapter. We know how to turn a collection into an iterator. How do we turn an iterator into a collection? Enter the collect method, provided by the Iterator trait. We could try to convert from a vector to an iterator and back. This, however, produces an error. What Rust is telling us here is that it doesn't know what we're trying to collect into. collect has a generic return type and could give you a number of things: a vector, a linked list, a string, etc. You could even create a custom type that can be collected into. To let Rust know what concrete type we want collect to return, we can use the turbofish syntax. We can make this slightly shorter. The compiler should be able to figure out that we want a vector of chars, specifically, and not a vector of integers. When filling out type parameters, we can use an underscore to tell Rust, "Figure this part out yourself!" In a case like this, you might find it tidier to add a type annotation to the variable declaration instead. The whole thing will then look like this: Using collect , we can convert an array of chars into a string. We could also collect an iterator of tuples (where the first element needs to be hashable) into a HashMap : Things that can be collected into implement the FromIterator trait. That means this behavior is extendable! Check out the trait's docs to see which types can be collected into and how to implement new ones. We now have some idea of how iterators work and some operations we can perform on them. Let's put it together and see some typical use cases. Let's say we store customer data in Customer structs, which include a customer's name, e-mail address, and how much they owe us. Then let's say we have a list of such customers. We're tasked with producing a vector of all debtor e-mails so we can send them a generic reminder. How do we do it? The nice, idiomatic way is to get an iterator over customers , apply some adapters that will filter and transform the data, and then collect that back into a vector. Given the same Customer struct as above, and the same vector of customers, we can search the customer data for a specific person. There's no useful method defined directly on the Vec<T> type, but there is a find method defined on the Iterator type. What if we'd like to get the position of an element in the vector? Things get a little trickier, but create an iterator. We then have to enumerate it to keep track of positions. enumerate will wrap every element in a tuple of formthe the (position, element) . Then we have to change our find closure a little to account for the items now being tuples. Finally, once we unwrap the Option<(usize, Customer)>, we still have to extract the position component of the tuple, which is the 0th one. The str type comes with a split method that yields an iterator over chunks of that string. All you have to provide is a pattern to split by - commonly a char , a &str or a String . For example, you could get all the words of a phrase this way: And then you could transform them and collect them into a new String : These are some examples of third-party libraries providing some functionality via iterators. This just about exhausts the core concepts and basic usage. Hopefully, you should now be able to not only effectively transform collections, but (with some practice) also identify where providing iterators would make sense in your code. One thing to do now would be to simply read the module-level documentation for std::iter , and take a look at the provided methods of the Iterator trait . There are some useful tools to discover there that we didn't cover here!

The Rust Map Function - A Gateway to Iterators

This post looks at the Map function in the Rust programming language as one of the basic tools to use and interact with collections and iterators.Arguably one of the main reasons the Rust language has become so popular is because the entire ecosystem builds upon a consistent experience. From toolchains to documentation and language specifications, almost every aspect of the language adheres to a certain level of consistency. Getting into the Rust mindset allows us, developers, to intuitively discover aspects of the language and gain the ability to write better and more consistent code. In this article, we are going to focus on Iterators - one of the key language tools enabling us to write idiomatic Rust code. This may surprise you at first, especially if you are coming from other programming languages (for example Swift or JavaScript), where working with types like lists and arrays does not immediately expose the concept of Iterators. The opposite is valid for Rust. Iterators cover a very large number of applications and as such, being comfortable with their use greatly facilitates becoming a better Rust developer. In order to follow along, you will need at least some familiarity with the Rust programming language. To make things easier, this article includes detailed code samples and links to the Rust playground where you can execute and study the provided code snippets. After reading this article, you will know: A core concept for Functional programming, Map is also commonly used as a tool for dealing with lists and collections in Rust and is one of the most essential methods for working with Iterators. The map method offers a way to apply a function (or a closure) on each element from a list. This is key in functional programming methodologies and offers a way to transform a collection of type A, to a collection of elements of type B. In Rust, the map method is frequently used to interact with vectors and any other types which can be represented as an Iterator . Let's illustrate this with an example. Imagine a list of numbers (a vector of numbers) and we would like to multiply each number by 10. Try it out for yourself. Let's review a breakdown of the steps You may be wondering why .map() doesn't work directly on the vector of numbers. The answer lies with the fact Rust makes it very easy to use iterators for almost everything. In the ergonomics of the language, it is generally preferred to use iterators instead of directly interacting with a vector. In the example above we actually have two iterators at play - the first which we obtain through the use of .iter() in order to get the elements of the vector and a second one which happens to be the return type of the .map() function! This may be a bit surprising at first, especially if you have a background in other programming languages (for example Swift or JavaScript) but indeed, in Rust .map() also returns an iterator. Let's have a closer look at the syntax we need in order to use Map. In the Rust documentation , we can find the exact signature of the Map method: With this we can focus on several key takeaways. .map() can only be applied to iterators. This means that we first need to convert a collection type to an iterator before we can transform its elements. To obtain an iterator from a vector, the standard library provides .iter() and .into_inter() . The return type of .map() is also an iterator. This can be very useful in cases when multiple transformations need to be chained one after the other. Let's illustrate this by extending the example with a second transformation of the vector of numbers. After multiplying by 10, let's divide each element by 3: Try it out for yourself. Since the Map function returns an Iterator, the result of the first transformation .map(|n| n * 10) will be used as input for the second transformation .map(|n| n / 3) . You probably notice by now that despite the return type of Map being Iterator, we somehow manage to get back a vector of results. This is possible thanks to the .collect() method. It acts as a consumer to the iterator, acquiring all elements and storing them into a vector. The Map function is lazy , meaning the execution of the closure provided to Map is delayed until the values produced by the Map iterator are actually requested. To demonstrate this, let's modify the example so that we count the number of times the closure of the map method is invoked: Every time .map() runs, it first increments the counter and then applies the transformation of multiplying the number by 10, similar to what we did in the previous example. As expected, the counter equals 4 because this is the total number of elements on which the map function was applied. Now let's modify this snippet by not calling .collect() at the end: Try it out for yourself. If we print the contents of the result variable, we see that what we get back is the actual Map iterator, the results of which are not materialized yet. We can prove this further by looking at the value of the counter variable which is now 0. This means the function inside the .map() has not been executed yet. The lazy nature of .map() gives us the opportunity to control when the transformation will be executed. You can imagine this can be significant in cases of large data sets where performance is important. Let's have a look at several examples in which the Map function can be a good solution. Try it out yourself. Starting from a vector of string values (or "words"), we can use the Map function in order to get the lowercased equivalent of each word. As before, we begin by accessing an iterator over the collection words. The .iter() method produces an iterator which yields every element (that is, every word) of the collection. Then we can use Map to define a closure which applies to_lowercase() on each element. In the end, we don't forget to .collect() the results into a new vector of String values. Let's define a string value and use Map in order to determine the number of occurrences of each character within the string. We expect an output giving the number of times each character is used, similar to the following: The result looks like a dictionary where every key is a character and the value of that key is the number of occurrences. Let's use the HashMap type to represent this value: Try it out for yourself. Here we use the chars method in order to obtain an iterator over every letter of the alphabet. Then we count how many times each letter matches the text input. For example, how many times "a" appears in "hello from rust!", then how many times "b" appears in "hello from rust!" etc. Finally, we collect the results from the mapping into a HashMap. So far we've used examples where the mapping function always succeeds. In reality, we have to take into account the circumstances when this is not going to be the case. Such cases include transformations that have an optional return type like Option or Result . Let's illustrate the case of using Map with an operation that can potentially fail. For example, converting a vector of string values to a vector of numeric values of type u32 . Applying what we've seen so far, we may attempt to solve the problem with the following snippet: Try it out for yourself. For each element of the vector, we use the parse method in order to convert the string value to a number. We realize quickly that this will not compile. The reason lies with the fact that Parse returns a Result<u32, std::num::ParseIntError> which indicates that the operation can either succeed returning a value of type u32 or fail with a ParseIntError . The map function is not equipped to deal with this situation directly. One way to solve the problem is to use .unwrap() , essentially forcing the fact that we are certain the parsing will never fail. If it does, the program will panic and exit: Try it out for yourself. This works because all string values in the source vector can be converted to a number. But what if we have a vector where some elements can't be expressed as a number? The unwrap fails and causes the program to panic. Luckily, the standard library provides an alternative that can handle the situation where the Map function can produce one or zero results. Meet flat_map() : Try it out for yourself! flat_map uses an iterator over the result of the mapping and as a consequence, it will skip over elements for which the mapping closure returns empty or unsuccessful values (like None or Err . In other words, we have a mechanism to skip over the failing elements and end up with a resulting vector of numbers for which the operation succeeded. A note of caution regarding side effects when using the Map function. Side effects refer to the fact that during the execution of a Map closure or a function, our program may modify state and variables which are external. We already saw an example of this, when we defined a counter which increments every time the Map function is executed: While this produces exactly the result we expect in this situation, it may not always be the case. Let's modify the example so that instead of transforming the numbers in the vector, we map the order in which they are processed: Try it out for yourself. This results in the Map function to first be applied to the number 3 on position 1, followed by the number 6 on position 2, etc. Since .map() returns an iterator, we could choose to modify its output. For example, let's reverse the order of the Map operation: Try it out for yourself. So the first number to be mapped was 12, followed by the number 9. The order in which the elements were mapped is no longer the same as the initial order in the vector. In order to ensure we have better control of operations with side effects, it may be preferred to use a for loop instead: Try it out for yourself. After seeing the Map function in action, we can outline several important takeaways. You may also find it useful to explore the Map function specification in the documentation which reveals all available methods and additional sample use cases and code snippets. When I need more information regarding an aspect of Rust, I find myself browsing the Rust Programming Language Book . I think it's an amazing resource for gaining further insight into concepts regarding Iterators, the Map method, or working with vectors. If you are curious about Iterators, you may want to further explore other helpful methods like .filter() and .fold() .

I got a job offer, thanks in a big part to your teaching. They sent a test as part of the interview process, and this was a huge help to implement my own Node server.

This has been a really good investment!

Advance your career with newline Pro.

Only $30 per month for unlimited access to over 60+ books, guides and courses!

Learn More