100-exercises-to-learn-rust/book/src/06_ticket_management/07_combinators.md

3.5 KiB

Combinators

Iterators can do so much more than for loops!
If you look at the documentation for the Iterator trait, you'll find a vast collections of methods that you can leverage to transform, filter, and combine iterators in various ways.

Let's mention the most common ones:

  • map applies a function to each element of the iterator.
  • filter keeps only the elements that satisfy a predicate.
  • filter_map combines filter and map in one step.
  • cloned converts an iterator of references into an iterator of values, cloning each element.
  • enumerate returns a new iterator that yields (index, value) pairs.
  • skip skips the first n elements of the iterator.
  • take stops the iterator after n elements.
  • chain combines two iterators into one.

These methods are called combinators.
They are usually chained together to create complex transformations in a concise and readable way:

let numbers = vec![1, 2, 3, 4, 5];
// The sum of the squares of the even numbers
let outcome: u32 = numbers.iter()
    .filter(|&n| n % 2 == 0)
    .map(|&n| n * n)
    .sum();

Closures

What's going on with the filter and map methods above?
They take closures as arguments.

Closures are anonymous functions, i.e. functions that are not defined using the fn syntax we are used to.
They are defined using the |args| body syntax, where args are the arguments and body is the function body. body can be a block of code or a single expression. For example:

// An anonymous function that adds 1 to its argument
let add_one = |x| x + 1;
// Could be written with a block too:
let add_one = |x| { x + 1 };

Closures can take more than one argument:

let add = |x, y| x + y;
let sum = add(1, 2);

They can also capture variables from their environment:

let x = 42;
let add_x = |y| x + y;
let sum = add_x(1);

If necessary, you can specify the types of the arguments and/or the return type:

// Just the input type
let add_one = |x: i32| x + 1;
// Or both input and output types, using the `fn` syntax
let add_one: fn(i32) -> i32 = |x| x + 1;

collect

What happens when you're done transforming an iterator using combinators?
You either iterate over the transformed values using a for loop, or you collect them into a collection.

The latter is done using the collect method.
collect consumes the iterator and collects its elements into a collection of your choice.

For example, you can collect the squares of the even numbers into a Vec:

let numbers = vec![1, 2, 3, 4, 5];
let squares_of_evens: Vec<u32> = numbers.iter()
    .filter(|&n| n % 2 == 0)
    .map(|&n| n * n)
    .collect();

collect is generic over its return type.
Therefore you usually need to provide a type hint to help the compiler infer the correct type. In the example above, we annotated the type of squares_of_evens to be Vec<u32>. Alternatively, you can use the turbofish syntax to specify the type:

let squares_of_evens = numbers.iter()
    .filter(|&n| n % 2 == 0)
    .map(|&n| n * n)
    // Turbofish syntax: `<method_name>::<type>()`
    // It's called turbofish because `::<>` looks like a fish
    .collect::<Vec<u32>>();

Further reading