We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Closures and Iterators step 22 of 28
size_hint and DoubleEndedIterator: an iterator with two ends
Implement an inclusive integer span that can be consumed from both ends and that reports how many items remain.
pub struct Span { pub lo: i64, pub hi: i64 }
pub fn span_probe(lo: i64, hi: i64, take_front: usize, take_back: usize)
-> (Vec<i64>, usize, usize)
Span { lo, hi } represents the integers lo..=hi, both ends included. It
is empty when lo > hi. The starter already implements Iterator::next,
which yields from the low end. Add:
- a
size_hintthat reports the exact number of remaining items; DoubleEndedIterator, whosenext_backyields from the high end;ExactSizeIterator.
The exercise contract
Every test uses lo and hi between -1_000_000_000 and 1_000_000_000
inclusive, so the remaining count always fits in usize and stepping past
either end cannot overflow i64. take_front and take_back can be any
non-negative value, including zero and values larger than the span.
The starter also contains a complete span_probe; you do not need to change
it. In this order, it:
- records
span.len(), which comes fromExactSizeIterator; - records
span.size_hint().0, the lower bound of the hint; - repeats rounds until both budgets are zero. In each round it first takes
one element with
nextiftake_frontbudget remains, then one withnext_backiftake_backbudget remains. Each taken element is pushed onto the output vector. The loop stops early the first time either call returnsNone.
Both counts are recorded before anything is consumed, so they describe the
whole span. The function returns (taken, len, hint_lower), and the grader
reports it as JSON with the keys taken, len and hint.
span_probe(1, 5, 2, 1) -> ([1, 5, 2], 5, 5)
span_probe(1, 4, 10, 10) -> ([1, 4, 2, 3], 4, 4)
span_probe(5, 1, 2, 2) -> ([], 0, 0)
span_probe(0, 0, 0, 0) -> ([], 1, 1)
In the first example, round one takes 1 from the front and 5 from the
back. Round two takes 2 from the front; the back budget is already spent.
In the second, the budgets are larger than the span, so each of the four
values appears once and the next call returns None. The third span is
empty. The fourth span contains only 0, but zero budgets mean nothing is
taken.
What size_hint reports
fn size_hint(&self) -> (usize, Option<usize>)
The first value is a lower bound on how many items remain. The second is an
upper bound, or None when the iterator cannot give one. The default
implementation returns (0, None). That is always a valid answer because it
promises nothing, but callers cannot use it to plan ahead.
The standard library treats the hint mainly as an optimization input. For
example, Vec and String collection and extend code currently use the
lower bound to reserve space before inserting items. This is an
implementation detail, not a documented guarantee about the number of
allocations. With (0, None), a vector starts small and grows as needed.
Adapters combine the hints of the iterators they wrap. On Rust 1.98.1,
with v a 20-element vector and chunks a Vec<Vec<u32>>:
| Expression | size_hint() |
|---|---|
(0..4).map(f) |
(4, Some(4)) |
v.iter().chain(v.iter()) |
(40, Some(40)) |
v.iter().rev() |
(20, Some(20)) |
v.iter().filter(p) |
(0, Some(20)) |
chunks.iter().flatten() |
(0, None) |
chunks.iter().flat_map(f) |
(0, None) |
"a,b,c".split(',') |
(0, None) |
map produces one output per input, so it passes the hint through. chain
adds the two hints. filter may reject every item, so its lower bound is
0, but it can never produce more than its input. A fresh flatten or
flat_map over vectors cannot know the inner lengths before reaching them,
so it reports no upper bound. str::split does not compute a count in
advance and also reports (0, None).
Some fixed-size cases, such as flattening arrays, can report more precise hints. The table shows only the expressions listed.
Why a correct hint matters
Rust's documentation says that an incorrect size_hint must not cause
memory unsafety, and unsafe code must not rely on it for that reason. It is
still a bug for an iterator to yield fewer items than its lower bound or
more than its upper bound.
A wrong hint can also produce wrong results, not just extra work. Some
standard methods and adapters use the length reported by
ExactSizeIterator. For example, iterating enumerate().rev() computes
each index from that length. When the hint is wrong, the indices are wrong.
This exercise returns the reported length and lower bound so the tests can
check them directly.
The standard library has an unstable unsafe marker trait, TrustedLen, for
iterators whose hint is guaranteed exact. It lets internal Vec code fill a
preallocated buffer with fewer checks. Stable code cannot implement it for
its own iterators, so Span cannot opt into that optimization. A correct
hint still lets ordinary collection code reserve the right capacity.
ExactSizeIterator relies on the hint
ExactSizeIterator promises that size_hint is exact. Its provided len
method is equivalent to:
fn len(&self) -> usize {
let (lower, upper) = self.size_hint();
assert_eq!(upper, Some(lower));
lower
}
If the two bounds disagree, len panics in both debug and release builds.
With a hint of (3, Some(4)), the message includes:
assertion `left == right` failed
left: Some(4)
right: Some(3)
The assertion cannot detect a hint whose bounds agree but are both wrong.
A hint of (3, Some(3)) for four remaining items returns 3 without a
panic. That is why the tests compare len with the true count.
Once size_hint is exact, impl ExactSizeIterator for Span {} needs no
method bodies.
Some standard ranges do not implement ExactSizeIterator at all, because
their length may not fit in usize on every target. For example,
(0i64..5).len() and (0i32..=5).len() are rejected with E0599, while
(0i32..5).len() compiles. The input bounds above avoid that problem for
Span.
DoubleEndedIterator: both ends share one sequence
fn next_back(&mut self) -> Option<Self::Item>
next_back removes an item from the other end of the same sequence. The
standard documentation describes the two ends as working on the same range
and not crossing. Methods such as rev, rfind, rfold and nth_back
use this trait. rposition also requires ExactSizeIterator.
For Span, next moves lo up and next_back moves hi down. When
lo > hi, no items remain. An off-by-one error can make both ends return the
middle element. With span_probe(10, 12, 3, 3), the correct output is
[10, 12, 11]; the next call returns None, so 11 must not appear twice.
last is an Iterator method. Its default implementation consumes items
from the front until the end, even if the iterator also implements
DoubleEndedIterator. Some iterators override it. Clippy 1.98 enables the
double_ended_iterator_last lint by default and suggests next_back when
calling last would needlessly walk the whole iterator.
FusedIterator
An ordinary iterator may return Some again after returning None.
FusedIterator is a marker trait promising that, after the first None,
every later call to next also returns None. The standard library uses it
to make Iterator::fuse cheaper. Any iterator can be wrapped with .fuse()
to get that behavior.
Within this exercise's input bounds, Span behaves this way: once lo > hi,
neither method changes the fields again. You do not need to implement
FusedIterator for these tests.
Passing requires compilation, the expected outputs and Clippy with warnings treated as errors.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.