Tutorials on Functional Programming

Learn about Functional Programming 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

What Rust Is All About: A Small Tour

In this post, we'll try to give you a taste of Rust's key features so you may get a feel for the language. We'll touch on the type system, some handy functional features, traits, and more!Rust is a systems programming language that was conceived at Mozilla and is now developed by a larger community. Initially, its purpose was to help rewrite portions of Firefox's codebase (largely C/C++) to improve its safety and security. It has since enjoyed growing adoption in fields such as Rust is a statically typed language that emphasizes static code analysis, helpful compilation errors, convenient and comprehensive tooling, memory and concurrency safety, sheer performance, and abstractions that incur no (or little) runtime cost. Its memory model avoids the overhead of a garbage collector, but at the same time, the language's compiler enforces a set of rules that effectively prevent memory errors common in C/C++ applications. This is not a comprehensive "learn Rust in one hour" guide. It should, however, give you a little taste of what the language is about and help you decide if it's for you! We will guide you through some of the key concepts you'll inevitably stumble into in Rust code, including custom types, error handling, ownership, closures, higher-order functions, iterators, pattern matching, and traits. This guide is intended for people with some prior high-level programming experience, though which particular language(s) you've previously worked with shouldn't overly matter. If you'd like to give the snippets a run, the easiest way is to use the Rust Playground online. If you prefer to run things locally, the Getting Started chapter from the Book will help you get your environment and a "Hello, World" project set up! A comprehensive static type system means safer code and fewer manual tests that need to be written. The more information you can encode in the type system, the more Rust can check for free during compilation, allowing greater confidence in the correctness and reliability of your programs! Rust has structs. Structs have fields. Rust also has enums for when a thing can be either this or that. Enums have variants. An enum value can only be set to one variant. Enum variants can hold data. Both structs and enums can be generic and accept arbitrary concrete types inside. This is possible by declaring type variables in angle brackets ( <> ). In the example above, the compiler will deduce that foo is of type Wrapper<f64> . Wrapper<T> is not a dynamic type! It's resolved during compilation. Oh, and by the way, the let statement is how you declare a local variable. It accepts type annotations, but most of the time they're not necessary - Rust might be a disciplined and strongly-typed language, but it's fairly smart and able to infer types itself a lot of the time. You can add methods to any type you define using impl blocks. Rust doesn't have anything like a try...catch statement. Instead, there is the Result<T, E> type, defined more or less like so: Two types should be provided here. T is the type of the actual return value. E is the type of error data. A function that may fail should return a Result - like the one below that attempts to sum a vector, but fails if it's empty. The unit type () is often used to signify nothing is returned. In this case, we're basically saying we don't want to add any extra error information if the function fails - we just want to know that it failed. In "real" Rust code, it's generally a good idea to provide an error type that implements the std::error::Error trait, but that's outside of our scope right now. If we want to use our my_sum function now, we cannot ignore the fact it might produce an error. The snippet below won't work. This doesn't compile. The compiler tells us the result of summing is really a Result , and so cannot be added to a float. First, we need to unpack the value inside the Result and handle the potential error. One verbose way to handle the error (by crashing the whole application) looks like this: This match statement isn't some special construct for error-handling. It can be used for pattern-matching against any enum. We'll talk about it later. This is the power of Rust's approach to error-handling: there's no magic. You get to use the same control flow as you would for the rest of your code. There's of course shorthand for the above: Another strong point for Rust is that even if you don't use the Ok value (sometimes there is none), the compiler will still nudge you about having unchecked results. Forgetting about potential problems isn't easy. Unless we go into unsafe hackery, Rust doesn't have null values. Let's define a struct Foo. Any initialized variable of the type Foo must hold a valid Foo with a valid f64 number inside. Thanks to that, if you write a function that accepts Foo , you don't have to worry about handling a potentially null value. The compiler will make sure only valid, initialized data gets passed to the function. What if we want to express that a variable can hold nothing? This is one place where generic enums come in handy. The standard library defines this Option type: If we wrap our Foo in an Option , we can express that we might have a Foo or nothing. Every computer program has to manage the memory used for its data. When some data is no longer used, the memory it occupied should be freed. After it is freed, the program should never attempt to access that data again, or face bugs. Most modern languages use garbage collectors . Thanks to those, this de-allocation and proper use are not something the programmer needs to be concerned with. This, however, introduces some performance overhead. Unlike most high-level languages, Rust doesn't have a garbage collector. Instead, there's the model of ownership and borrow checking. Ownership is a key concept in Rust. The idea is that, at any time, every bit of data in memory has exactly one owner. When that owner goes out of scope, the data is tossed out. It is also possible to create references to owned data. These do not affect how long the data lives, but for your program to compile, the compiler has to ascertain that the references aren't used after the owner goes out of scope and the data is no more. If you try to compile the above snippet, you'll get this: Try to comment out the last println! statement and run the snippet then! By default, a reference is immutable. To create a mutable reference, you have to add the mut keyword like so: The first mut keyword allows mutation to the owned data at all. The &mut foo creates a mutable reference, which is then stored in foo_ref . *foo_ref dereferences the reference to get access to the underlying data and modify it through assignment. You can have many immutable references, but you can only have one mutable reference. While you hold a mutable reference, you cannot use the owner or have any other (mutable or not) reference. This is to prevent data races and mistakes. All illegal use of references causes compilation errors and forces you to fix your code before it ever has a chance to see production! One amazing thing about Rust is it brings some high-level abstractions inspired by cutting edge languages to the world of systems programming. Rust supports closures - anonymous functions capable of dynamically capturing the variables from the encompassing scope. These can be treated like any other value and assigned to variables. Those variables can then be called the same way regular functions can. Notice that num_printer prints two numbers, but they're passed through different mechanisms. The x argument has to be provided when calling num_printer , just like with named function. num_to_capture is implicitly captured by num_printer when the function is defined. This becomes much more interesting when combined with higher-order functions - functions that accept other functions as arguments and/or return a function. Say a third-party library defines a function called perform_calculations and allows you to extend the behavior of it by providing a callback. This is your chance to get partial results printed for the user currently logged in. Iterators are a major feature of Rust. They can be very convenient. Ranges of numbers are iterators. Iterators can be iterated on in a for loop. A borrowing iterator can be created for any collection, e.g. an array. It's possible to turn your own custom types into iterators by implementing the Iterator trait, too. An example can be found here ! There's one more way to execute something on every element of an iterator. What if we want to reverse an array, add an m to every element, and then collect it into a vector of strings? What if we want to get the product of numbers from 1 to 5 (not inclusive)? fold lets us apply a function to the first and second elements, then to the result and the third element, and so on. Hopefully, this gives you a taste of the power of iterators. They're not just for looping over them; they're a convenient and powerful tool for transforming data. The best way to learn about all those methods is to browse the Iterator documentation. That's also where you can learn to implement custom iterators - all you need to do is implement the trait on your own type. Rust allows pattern matching and all the de-structuring goodness associated with it. An underscore ( _ ) is used as a catch-all - it matches anything. Tuples can be de-structured while we're pattern-matching against them. Underscores can also be used inside those patterns. Here's an implementation of Fizz buzz : Enums can be neatly de-structured too. Instead of matching specific values inside of structures, they can be captured in a variable. Traits essentially declare some functionality that concrete types may provide. Traits can be compared to interfaces in other languages or (more accurately) to typeclasses in Haskell. They are Rust's solution to the problem of code reuse. They can pretty much act as abstract types, too. One important property that traits have and interfaces in other languages do not is that traits can be implemented for foreign types - even built-in primitives. It's entirely possible to extend f64 with your own, custom trait. One example of a useful trait is the Iterator one. It's fairly straightforward to create a custom iterator. The one below returns positive integers in order, ad infinitum. After we have one defined, we can use it like any other iterator, including all of those convenience functions mentioned earlier. They are provided "for free" for anything that implements the Iterator trait. Since many operators are either implemented using traits or defer to a trait when faced with custom types, operator overloading is possible by implementing those traits. It's usually a reasonably straightforward endeavor. Here's how addition can be implemented. Trait bounds are what make generic programming powerful. Previously, we mentioned type variables. It's possible to declare those for functions, too. There's not much we can safely do with a value of an unknown type, though. What if we knew something more about it? What if we knew what traits it implements? Here we define the function print_it generically for any value that implements the Display trait and can therefore be printed as a user-friendly string. Thanks to the trait bound, we know we can safely print whatever type we get. It's important to stress that this isn't dynamic - the concrete types are inferred during compilation. When you call print_it(123.3) , the compiler (rather than the runtime) will check that f64 implements the Display trait, and then essentially create a function from the generic print_it definition specifically for the f64 type. What does it matter if this check is performed during compilation or runtime? If we ensure the correctness of types we pass around during compilation, type errors never have a chance to make it to production. It's much easier to rely on this kind of mechanism than to write extensive tests for all the problems we may create. There's some preferable syntax sugar for the generic print_it function. It helps us avoid thinking about type variables. Life is usually simpler that way! Whew! Hopefully, this helped you get a feel for Rust and shown it as the clever language it is. We've taken a glimpse of the type system, error handling, safety, functional patterns, pattern matching, and traits. Again, this tour isn't comprehensive; we've only scratched the surface here. The path to understanding the language and writing idiomatic code is much longer. Every journey has to start somewhere, though. We can only hope this was a good start, and that it got you hungry for more. Want to learn Rust properly? "The Book" is widely considered the best course; we cannot recommend it enough. You will learn everything we've mentioned here and more. Some alternatives:

Prelude to Vectors in the Rust Programming Language

In this post, we are going to explore in detail how to work with resizable arrays in Rust. Specifically, we will take a closer look at the Vector type, its syntax, and some use cases like filtering and transforming a collection.In software development, we often face the need to deal with a list of objects or values. For example, enumerating words or ingesting series of numeric values, parsing structured data from tables or data storage like CSV files or a database. Also referred to as collections, such data structures serve as a container of discrete values, offering a great facility to organise all kinds of dynamic data in a program. The Rust Standard Library includes several different kinds of collections like static arrays, tuples, vectors, strings and hashmaps. Our focus here is set on one of the more commonly used array-like types - the Vector type. Unlike arrays and tuples, collections of type Vector are dynamic which means they can be changed at runtime, making them a versatile and convenient data type. In this post, we are going to explore the following aspects of the Vector type in Rust: Finally, we are going to take a moment to review how Rust’s internal safety mechanisms protect the developer from performing potentially unsafe operations. This tutorial assumes you are familiar with the Rust language syntax and have a general understanding of how a program allocates memory (e.g. heap vs stack). We often use terms like collections and arrays to describe structures of numbered lists of items. The vector type in Rust is one example of such a structure and it is the most commonly used form of collection. It has the type Vec , it is pronounced “vector”. The basic structure of a vector can be seen as a combination of the following information: A vector can always be represented as a tripled of these 3 values: pointer, capacity, and length. Also, the basic nature of the Vec type allows us to make use of several guarantees provided by the Rust runtime. For example, the pointer of a vector can never be null as the Vector type is null pointer optimized. If a vector is empty (contains no elements) or all elements are zero-sized, then Rust ensures that no memory will be allocated for the vector. The capacity() of a vector indicates the number of elements that can be added to a vector without re-allocation of memory. It can be seen as a sort of reserved or pre-allocated memory. You can learn more about the guarantees and memory specifics of the vector type in the documentation . There are several ways we can define a new vector. A vector can be initialized using the Vec::new function which returns a new empty vector. Once created, the new vector variable can be marked as mutable (using the mut keyword) in order to be able to add and remove elements from it. Try it out for yourself. It is worth pointing out that we did not declare the type of elements we intend to add to the collection. Let's see what happens if we declare the vector as we did above, but don't insert any elements to it e.g.: This statement alone will not compile and the error message will be cannot infer type for type parameter "T" . This happens because the Vec type uses generics in order to specify the type of elements that will be added to the vector collection. In the first example, we added elements to the vector and so the Rust compiler was able to infer the type of the variable vec to be Vec<&str> as the elements being added to it are of type &str . In the second example, only initializing a new empty vector was not enough for the Rust compiler to determine what kind of elements we intend to store this causing raising a compiler error. In order to solve this, we can choose to explicitly specify the type of the vector collection during initialization. For example: The syntax of the Vec::new function may seem a bit verbose as we first need to initialize a mutable variable and only then add elements to it. Luckily, Rust also includes the vec! macro which adds certain facilities to make it easier to initialize new vectors by also providing the initial elements in the collection. We could rewrite our example in order to use the vec! macro instead of the Vec::new function as follows: Since the initial elements of the vector are known upfront, this can be made even more concise, by directly initializing the vector with the initial elements: Try it out for yourself. Now that we know how to create a vector, let's have a look at some of the techniques we can utilize in order to access the contents (or elements) of a vector. The length of the vector corresponds to the number of elements currently being stored by the vector. We can obtain the length using the len() function: The capacity of the vector is the number of elements that a vector can hold without the need to reallocate additional memory. A vector normally stores its elements in a memory buffer which can grow over time as new elements are being added to it. By default, the capacity of the vector is automatically adjusted as we add elements to the collection. When we create a new empty vector, we can choose to define an initial capacity, essentially reserving the initial buffer size of the vector. This means that when new elements are added, the vector will not have to reallocate additional memory as long as there is remaining space in its buffer (capacity). Let's illustrate with an example. Try it out for yourself. If we just create a new empty vector, it has 0 elements and a capacity of 0. This means adding an element will require the vector to first allocate some memory to increase its capacity to at least 1, in order to accommodate the incoming element. Of course, this works just fine and may not be a problem at all. For example, we can create a new empty vector with an initial capacity for 10 elements: The vector type in Rust implements the Index trait, allowing us to directly access elements by index: A common source of bugs and security vulnerabilities is what is commonly known as out of bounds access i.e. trying to access an element outside the length of a vector. While very easy to use, direct access by index has a downside - we may accidentally request an element index which is out of bounds which will cause the program to panic: To help with that, Rust offers an alternative using the Vec::get function which returns a value of type Option instead, allowing us to gracefully handle this scenario and as a result, improving the reliability of the program: Try it out for yourself. A mutable vector can be changed by adding or removing elements from it. We do this using the Vec::push and Vec::pop functions. Respectively they either append an element to the end of a vector or remove the last element of a vector. The Vec::pop method also returns an Option value which either holds the removed element or None if no elements were removed from the vector (e.g. when it was already empty). Try it out for yourself. In some situations it may be useful to update an element which already belongs to a vector: Try it out for yourself. You may notice that we are directly accessing an element by index. Like we saw earlier, given an index outside the bounds of the vector the application will panic. We can use a technique we showed earlier with the Vec::get method and its companion Vec::get_mut which returns a mutable reference to an element, if it exists. We can then rewrite the above example in a safer way as follows: Like get , the get_mut method returns an Option with a reference to the element at the given index. If the element doesn't exist (when the index is out of bounds), get_mut returns None . If the element exists, get_mut returns a mutable reference which we can use to update the value. If we would like to perform a certain operation over each element in a vector, we can iterate through all elements rather than accessing them one at a time. One way would be to use a for loop: In this case, we are consuming the vector by executing the operation defined in the for loop block over each element of that vector. We could also limit the operation to just references to the elements of the collection. Using the same technique, we can obtain a mutable reference to the elements, allowing us to affect changes to the collection: Try it out for yourself. Another powerful technique to access the elements of a vector is through means of an iterator. To obtain an iterator over a vector, we use the Vec::iter method: Try it out for yourself. Generally speaking, Rust makes it easy to use iterators for almost everything. In the ergonomics of the language, it is almost preferred to use iterators instead of directly interacting with a vector. An example use of iterators will be the case of transforming the values of a collection from one type to another. Given a collection of words, let's build a vector which holds the length of each word: Try it out for yourself. Iterators are a powerful concept in Rust and they prove to be very useful when we are interested in obtaining a subset of a given collection. We can use the Vec::filter method in order to filter the elements of a vector: Try it out for yourself. Nothing prevents our application from adding the same value multiple times to a vector. There are however circumstances when it may be needed to remove the duplicates from a collection. Imagine for example, if the vector is based on user-provided data and we are only interested in working with unique (non-repeatable) values. This is easy to achieve using the Vec::dedup method. Once called on an instance of a vector, dedup works on that same instance and removes consecutively repeated elements. This means that for the deduplication logic to work as we expect, the vector needs to be sorted so that repeating elements follow each other. For example [1, 3, 2, 3] will not work very well because the repeating values 3 are not adjacent to each other. Once the vector is sorted to [1, 2, 3, 3] we can make use of the Vec::dedup method in order to remove the repeating values. The Vec::deduce needs the elements of the vector to implement the PartialEr trait in order for the comparison to work. This means it can also work for custom structs, as long as they implement PartialEr . Let's check an example: Try it out for yourself. Here we declare the vector as mutable since Vec::dedup updates the contents of the collection in place. A common type to represent resizable arrays is the Vector type. It is one of the more versatile collection types, enabling a great deal of flexibility when accessing and working with its elements. In this post, we saw how to get started with using the vector type for common operations like filtering and transforming a collection of elements. In addition, we discussed some of the safety protections provided by the Rust runtime in such scenarios like guarding against out of bounds access or null pointers. You may also find it useful to explore the Vec specification from the Rust documentation where you can read about all available methods and additional sample use cases and code snippets. You can also check one of my other posts which cover additional use cases for using Iterators with vectors in Rust.

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

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() .