functional rust and why iterators are the best!

posted on 2022-08-17

basics of functional programming

when i first learned rust, functional programming was alien to me. i've used go before that, which is just imperative:

// parsing lines from stdin to ints

numbers := []int{}

scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
	i, err := strconv.Atoi(scanner.Text())
	if err != nil {
		panic(err)
	}
	numbers = append(numbers, i)
}

while in rust, you can use a more functional approach:

let numbers: Vec<i32> = std::io::stdin()
	.lines()
	.map(|line| line.parse().unwrap())
	.collect();

yes, you can also write it imperatively in rust, but this is more ergonomic, and thanks to rust's zero cost abstractions™, both of these approaches are basically equivalent in speed. a core concept of functional programming in rust is iterators.

iterators

this is a simplified definition of the Iterator trait.

trait Iterator {
	type Item;
	fn next(&mut self) -> Option<Self::Item>;
}

that's it.

this means, that if you want to make your own iterator, it's trivially simple. but the iterator trait contains more methods, that build on top of next. there are multiple types of methods:

adapters

these are some cool adapters, keep in mind, that there are way more, than i can fit into this article

.map

the .map method of iterators can convert between types. it's only parameter is a closure, which takes the iterator's value as input, and returns a value. it returns an iterator, which calls the closure on each item.

.filter

.filter takes a closure, which returns a bool, if it's true, keep the item, if it's false, ditch it.

.fold

.fold takes an initial value of the accumulator, and a closure that takes the accumulator, and the next item. it returns the value, which will be used as the accumulator for the next iteration. this can be used for example to multiply numbers.

// let's multiply numbers from 1 to 5 (inclusive).
(1..=5)
//        ↓ initial accumulator
	.fold(1, |acc, n| acc*n);

.take

.take takes a number as argument, and takes only the specified number of elements.

// this will take numbers from 1 to 5 (inclusive).
(1..).take(5);

consumers

.collect

.collect takes an iterator, consumes it, turning it into a collection (for example a Vec)

let vec: Vec<_> = (1..=5).collect();

when using collect, the collection may not be type inferred, so you might need to specify the type either this way, or using turbofish syntax.

.count

.count takes an iterator, consumes it, and returns the number of elements in it.

laziness

in rust, iterators are "lazy", which means they do nothing until consumed, which is pretty interesting.

both the code, and content for this website are licensed under the Unlicense