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.
You don't need the T
type parameter on your function; you can write the bound I::Item: Display
to get the same effect.
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
.
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()
}
}