4
\$\begingroup\$

Just for fun, I created this small function that prints an iterator in a String:

use std::fmt::{Display, Write};

fn enumerate<T: Display, I: IntoIterator<Item = T>>(it: I) -> String {
    let mut it = it.into_iter().peekable();
    let mut result = String::new();

    if let Some(first) = it.next() {
        write!(result, "{}", first).unwrap();
        if it.peek().is_some() {
            while let Some(item) = it.next() {
                match it.peek() {
                    Some(_) => write!(result, ", {}", item),
                    None => write!(result, " and {}", item),
                }.unwrap();
            }
        }
    }

    result
}

fn main() {
    assert_eq!(enumerate(Vec::<i32>::new()), "");
    assert_eq!(enumerate(vec![1]), "1");
    assert_eq!(enumerate(vec![1, 2]), "1 and 2");
    assert_eq!(enumerate(vec![1, 2, 3]), "1, 2 and 3");
    assert_eq!(enumerate(vec![1, 2, 3, 4]), "1, 2, 3 and 4");
}

But I am not really satisfied with it. It is not really readable, and I am switching between next and peek in nested blocks. I wonder if this is possible to write the function in a more functional manner?

\$\endgroup\$
4
\$\begingroup\$
  1. It's often more readable to write trait bounds (e.g. T: Display) in a where clause rather than directly next to the declaration. Also, there are some types of bounds that cannot be written with the angle brackets and must instead be written in a where clause (there are examples below). When you have a where clause, put all the bounds here; don't put bounds in both places, because then it's easy for someone reading the code to miss the bounds in the angle brackets.

  2. You don't need the T type parameter on your function; you can write the bound I::Item: Display to get the same effect.

  3. The if it.peek().is_some() is unnecessary; if that expression returns false, the program wouldn't enter the while let loop anyway, and that loop is the only statement in the if.

  4. By using DoubleEndedIterator::next_back, we can extract the last item from the iterator. This lets us remove the peeks to determine whether you should use a comma or "and" as the separator. However, this restricts the types of iterators that the function accepts, because the iterator must now implement DoubleEndedIterator. In order to write this requirement, we must write the bound I::IntoIter: DoubleEndedIterator in a where clause; we can't write this within the angle brackets.

fn enumerate<I>(it: I) -> String
where
    I: IntoIterator,
    I::Item: Display,
    I::IntoIter: DoubleEndedIterator,
{
    let mut it = it.into_iter();
    let mut result = String::new();

    if let Some(first) = it.next() {
        write!(result, "{}", first).unwrap();

        if let Some(last) = it.next_back() {
            while let Some(item) = it.next() {
                write!(result, ", {}", item).unwrap();
            }

            write!(result, " and {}", last).unwrap();
        }
    }

    result
}

I wonder if this is possible to write the function in a more functional manner?

Building upon the code above, we can replace the while let loop with a fold.

fn enumerate<I>(it: I) -> String
where
    I: IntoIterator,
    I::Item: Display,
    I::IntoIter: DoubleEndedIterator,
{
    let mut it = it.into_iter();
    let mut result = String::new();

    if let Some(first) = it.next() {
        write!(result, "{}", first).unwrap();

        if let Some(last) = it.next_back() {
            result = it.fold(result, |mut result, item| {
                write!(result, ", {}", item).unwrap();
                result
            });

            write!(result, " and {}", last).unwrap();
        }
    }

    result
}

write! doesn't "feel" functional, though, because it mutates its operand rather than returning a new value. Therefore, in this case, I don't think the version with fold is really more readable than the version with while let.

By using itertools, a third-party crate published on crates.io, we can simplify the code a bit and give it a more functional feel by using its join adapter. Its implementation is more or less what we were doing before.

extern crate itertools;

use std::fmt::{Display, Write};
use std::iter;

use itertools::Itertools;

fn enumerate<I>(it: I) -> String
where
    I: IntoIterator,
    I::Item: Display,
    I::IntoIter: DoubleEndedIterator,
{
    let mut it = it.into_iter();

    if let Some(first) = it.next() {
        if let Some(last) = it.next_back() {
            let mut result = iter::once(first)
                .chain(it)
                .join(", ");

            write!(result, " and {}", last).unwrap();
            result
        } else {
            format!("{}", first)
        }
    } else {
        String::new()
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.