Question
I have Rust code like this, which compiles and runs correctly:
fn main() {
let text = "abc";
println!("{}", text.split(' ').take(2).count());
}
Then I tried to move the split logic into a separate function:
fn main() {
let text = "word1 word2 word3";
println!("{}", to_words(text).take(2).count());
}
fn to_words(text: &str) -> &Iterator<Item = &str> {
&(text.split(' '))
}
This does not compile, and I am not sure what the correct return type of to_words() should be.
The compiler reports that methods like count() are not available because the trait bounds are not satisfied.
What is the correct way to write to_words() so this works, and what Rust concept am I missing about returning Iterator or other traits from functions?
Short Answer
By the end of this page, you will understand why Iterator itself cannot usually be returned by reference the way a concrete type can, and how Rust handles returning trait-based values. You will learn when to use impl Iterator, when to use trait objects like Box<dyn Iterator<...>>, how lifetimes apply to iterators that borrow data, and why your original function failed.
Concept
In Rust, Iterator is a trait, not a concrete type. That distinction is the key to your question.
When you write:
text.split(' ')
Rust creates a concrete iterator type. That concrete type is something like std::str::Split<'a, char>. It implements the Iterator trait.
Trait vs concrete type
A trait describes behavior:
Iteratorsays a type can produce items one by one.- It does not by itself tell Rust how much memory the value needs.
A concrete type describes an actual value in memory:
Split<'a, char>has a known size.- Rust can return it directly from a function.
That is why this works conceptually:
fn to_words(text: &str) -> std::str::Split<'_, char> {
text.split(' ')
}
But in practice, Rust programmers usually write:
fn to_words(text: &str) -> impl Iterator<Item = &str> {
text.split(' ')
}
Why &Iterator<Item = &str> is wrong here
Your function tried to return:
fn to_words(text: &str) -> &Iterator<Item = &str>
This has multiple problems:
1. Iterator is a trait, not a concrete return type
A bare trait type does not have a known size at compile time, so Rust cannot return it by value directly.
2. You returned a reference to a temporary value
This expression:
&(text.split(' '))
creates an iterator temporarily, then tries to return a reference to it. That temporary value is destroyed when the function ends, so the reference would point to invalid memory.
Rust prevents that.
3. Trait object references have limitations
A reference like &dyn Iterator<Item = &str> can exist, but it refers to some iterator value stored elsewhere. Your function does not have such a long-lived stored iterator to reference.
The knowledge gap
The missing concept is:
- Traits describe capabilities
- Functions return concrete types unless you explicitly use:
impl Traitfor one hidden concrete typedyn Traitbehind a pointer likeBoxor&
For iterators, the most common choice is impl Iterator.
Why this matters in real Rust
Returning iterators is extremely common because it lets you:
- avoid allocating intermediate collections
- build lazy data pipelines
- keep APIs flexible and efficient
- hide complicated concrete iterator types
This is one of Rust's strengths: you can return efficient iterator chains without exposing their full internal type.
Mental Model
Think of a trait like a job description, and a concrete type like an actual worker.
Iterator= "someone who can hand out items one at a time"Split= the specific worker doing that job
If a function says it returns an Iterator, that is like saying:
- "I will send you a worker with these abilities"
But Rust still needs to know how that worker is represented in memory.
There are three common ways to handle that:
- Return the exact worker type
- "I return a
Split<'a, char>"
- "I return a
- Return
impl Iterator- "I return some specific worker that implements
Iterator, but I won't tell you which one"
- "I return some specific worker that implements
- Return a boxed trait object
- "I return any worker that implements
Iterator, wrapped in a box"
- "I return any worker that implements
Your original code tried to return a reference to a temporary worker who disappears as soon as the function ends. That is like handing someone a note saying, "Ask that worker over there," after the worker has already left the building.
Syntax and Examples
Recommended approach: impl Iterator
This is the most common and cleanest solution.
fn main() {
let text = "word1 word2 word3";
println!("{}", to_words(text).take(2).count());
}
fn to_words(text: &str) -> impl Iterator<Item = &str> {
text.split(' ')
}
Why this works
text.split(' ')returns a concrete iterator type.impl Iterator<Item = &str>tells Rust:- the function returns some concrete type
- that type implements
Iterator - the items are
&str
Rust still knows the real type internally, so there is no dynamic dispatch or heap allocation here.
Returning the exact concrete type
You can also return the exact iterator type:
fn to_words(text: &str) -> std::str::Split<'_, char> {
text.split(' ')
}
This works, but it exposes implementation details. If you later change from split(' ') to something else, the function signature may need to change.
Returning a boxed trait object
Use this when different branches may return different iterator types.
fn to_words(text: &str) -> Box<dyn Iterator<Item = &str> + '_> {
Box::new(text.split(' '))
}
Why + '_ is there
The iterator borrows from text, so the returned trait object must not outlive text.
Example with filtering
fn non_empty_words(text: &str) -> impl Iterator<Item = &str> {
text.split(' ').filter(|word| !word.is_empty())
}
fn main() {
let text = "word1 word2 word3";
for word in non_empty_words(text) {
println!("{word}");
}
}
This shows how useful impl Iterator is: the real return type would otherwise be long and awkward because filter(...) creates another iterator type.
Step by Step Execution
Consider this code:
fn to_words(text: &str) -> impl Iterator<Item = &str> {
text.split(' ')
}
fn main() {
let text = "word1 word2 word3";
let count = to_words(text).take(2).count();
println!("{count}");
}
Step by step
1. text is created
let text = "word1 word2 word3";
text is a string slice, &str.
2. to_words(text) is called
to_words(text)
Inside to_words:
text.split(' ')
creates a Split iterator that borrows from text.
3. The iterator is returned
The function returns that iterator as impl Iterator<Item = &str>.
This does not collect anything yet. Iterators are lazy.
4. .take(2) wraps the iterator
to_words(text).take(2)
This creates a new iterator that will yield at most 2 items.
Still, nothing has been consumed yet.
5. .count() consumes the iterator
.count()
Now Rust starts pulling values:
- first item:
"word1" - second item:
"word2" - stop because
take(2)only allows 2 items
6. The result is printed
println!("{count}");
Output:
2
Important idea
The returned iterator borrows from text, so it can only be used while text is still valid. Rust checks this for you with lifetimes.
Real World Use Cases
Returning iterators is useful in many practical situations.
1. Parsing text input
fn lines_without_empty(text: &str) -> impl Iterator<Item = &str> {
text.lines().filter(|line| !line.trim().is_empty())
}
Used in:
- log parsing
- config file reading
- command output processing
2. Tokenizing user input
fn tokens(input: &str) -> impl Iterator<Item = &str> {
input.split_whitespace()
}
Used in:
- CLI tools
- search queries
- simple interpreters
3. Processing API or file data lazily
You may not want to allocate a Vec immediately.
fn csv_fields(line: &str) -> impl Iterator<Item = &str> {
line.split(',')
}
This is useful when you only need some fields or want to process data one piece at a time.
4. Building reusable data pipelines
fn valid_ids(text: &str) -> impl Iterator<Item = &str> {
text.split(',').map(str::trim).filter(|s| !s.is_empty())
}
This pattern keeps code modular without forcing allocation.
5. Library APIs
Rust libraries often return iterators so callers can choose what to do next:
collect()into a vectorcount()itemsfind()one itemmap()into another shape
Real Codebase Usage
In real projects, developers often return iterators to keep code efficient and expressive.
Common pattern: hide complex iterator types
fn active_usernames(data: &str) -> impl Iterator<Item = &str> {
data.lines()
.filter(|line| !line.is_empty())
.map(|line| line.trim())
}
The actual return type here is a deeply nested iterator type. impl Iterator keeps the API readable.
Common pattern: validation before iteration
Sometimes developers use a guard clause before building the iterator.
fn words_or_empty(text: &str) -> impl Iterator<Item = &str> {
text.split_whitespace()
}
Or if different logic is needed, they may return a boxed iterator:
fn choose_iter(text: &str, trim: bool) -> Box<dyn Iterator<Item = &str> + '_> {
if trim {
Box::new(text.split_whitespace())
} else {
Box::new(text.split(' '))
}
}
Common pattern: chaining in the caller
Functions often return an iterator and let the caller decide how to consume it.
let first_two: Vec<_> = to_words(text).take(2).collect();
let total = to_words(text).count();
let has_admin = to_words(text).any(|w| w == "admin");
This makes utility functions flexible.
Common pattern: prefer borrowed data when possible
Returning &str items avoids extra allocation.
fn tags(line: &str) -> impl Iterator<Item = &str> {
line.split(',').map(str::trim)
}
This is common in parsers, config readers, and text-processing tools.
When teams avoid boxed iterators
Box<dyn Iterator<...>> is useful, but many codebases prefer impl Iterator when possible because:
- no heap allocation
- no dynamic dispatch
- simpler performance model
Common Mistakes
1. Returning a reference to a temporary iterator
Broken code:
fn to_words(text: &str) -> &dyn Iterator<Item = &str> {
&(text.split(' '))
}
Why it fails
The iterator created by text.split(' ') is temporary and is dropped when the function ends. A reference to it would be invalid.
Fix
Return the iterator by value:
fn to_words(text: &str) -> impl Iterator<Item = &str> {
text.split(' ')
}
2. Confusing traits with types
Broken idea:
fn make_iter() -> Iterator<Item = i32> {
0..10
}
Why it fails
Iterator is a trait, not a concrete sized type.
Fix
Use either:
fn make_iter() -> impl Iterator<Item = i32> {
0..10
}
or:
fn make_iter() -> Box<dyn Iterator<Item = i32>> {
Box::new(0..10)
}
3. Forgetting that iterators can borrow
Broken code:
fn to_words() -> impl Iterator<Item = &str> {
let text = String::from("a b c");
text.split(' ')
}
Why it fails
The returned iterator would borrow from text, but text is dropped at the end of the function.
Fix
Either accept borrowed input:
fn to_words(text: &str) -> impl Iterator<Item = &str> {
text.split(' ')
}
Or return owned strings instead.
4. Using split(' ') when split_whitespace() is better
fn to_words(text: &str) -> impl Iterator<Item = &str> {
text.split(' ')
}
This may produce empty strings when there are multiple spaces.
Often this is better:
fn to_words(text: &str) -> impl Iterator<Item = &str> {
text.split_whitespace()
}
5. Using impl Iterator when multiple branches return different iterator types
Broken example:
fn choose(text: &str, mode: bool) -> impl Iterator<Item = &str> {
if mode {
text.split(' ')
} else {
text.lines()
}
}
Why it fails
impl Iterator still means one concrete type. Both branches must return the same actual type.
Fix
Use a boxed trait object:
fn choose(text: &str, mode: bool) -> Box<dyn Iterator<Item = &str> + '_> {
if mode {
Box::new(text.split(' '))
} else {
Box::new(text.lines())
}
}
Comparisons
| Approach | Example | Best for | Pros | Cons |
|---|---|---|---|---|
| Concrete type | fn f(s: &str) -> std::str::Split<'_, char> | Simple cases where you do not mind exposing internals | Fast, explicit, no allocation | Ties API to implementation details |
impl Iterator | fn f(s: &str) -> impl Iterator<Item = &str> | Most iterator-returning functions | Clean API, no heap allocation, no dynamic dispatch | Must return one concrete type |
| Boxed trait object | fn f(s: &str) -> Box<dyn Iterator<Item = &str> + '_> | Different branches returning different iterator types | Flexible, hides type completely | Heap allocation and dynamic dispatch |
| Reference to trait object | fn f(...) -> &dyn Iterator<Item = ...> | Rare cases where iterator is stored elsewhere | Can avoid ownership transfer | Hard to use correctly; cannot reference temporaries |
impl Trait vs dyn Trait
| Feature | impl Iterator | dyn Iterator |
|---|---|---|
| Dispatch | Static | Dynamic |
| Heap allocation required | No | Usually yes, with Box |
| Can hide concrete type | Yes | Yes |
| Can return different concrete types from branches | No | Yes |
| Performance | Usually better | Slight overhead |
split(' ') vs split_whitespace()
| Method | Behavior |
|---|---|
split(' ') | Splits only on the space character and may produce empty items |
split_whitespace() | Splits on any whitespace and skips repeated whitespace cleanly |
For word parsing, split_whitespace() is often the better choice.
Cheat Sheet
Quick rules
Iteratoris a trait, not a concrete type.- Return iterators by value, not by reference to a temporary.
- Use
impl Iterator<Item = T>for most cases. - Use
Box<dyn Iterator<Item = T> + 'a>when different branches return different iterator types. - If the iterator borrows from input, the returned iterator must carry that lifetime.
Common patterns
Return an iterator borrowing from input
fn words(text: &str) -> impl Iterator<Item = &str> {
text.split_whitespace()
}
Return exact iterator type
fn words(text: &str) -> std::str::SplitWhitespace<'_> {
text.split_whitespace()
}
Return boxed trait object
fn words(text: &str) -> Box<dyn Iterator<Item = &str> + '_> {
Box::new(text.split_whitespace())
}
Lifetime reminder
If items come from text, the iterator usually borrows from text.
That means this is fine:
fn words(text: &str) -> impl Iterator<Item = &str> {
text.split_whitespace()
}
But this is not:
fn words() -> impl Iterator<Item = &str> {
let s = String::from("a b c");
s.split_whitespace()
}
Good default answer for this question
fn to_words(text: &str) -> impl Iterator<Item = &str> {
text.split_whitespace()
}
FAQ
Why can't I return Iterator directly in Rust?
Because Iterator is a trait, not a concrete sized type. Rust needs either a concrete type, impl Iterator, or a trait object like Box<dyn Iterator<...>>.
Should I use impl Iterator or Box<dyn Iterator>?
Use impl Iterator by default. Use Box<dyn Iterator> when you need to return different concrete iterator types from different branches.
Why did returning &Iterator fail?
You were returning a reference to a temporary iterator created inside the function. That temporary is dropped when the function ends.
Does impl Iterator allocate memory on the heap?
No. impl Iterator uses static dispatch and does not require heap allocation by itself.
What lifetime does the returned iterator have?
If the iterator borrows from text, its lifetime is tied to text. Rust usually infers this automatically.
Is split(' ') the same as split_whitespace()?
No. split(' ') splits only on the space character and can return empty strings. split_whitespace() handles repeated spaces and other whitespace more cleanly.
Can I return different iterators with impl Iterator?
Not unless they are the same concrete type. If branches produce different iterator types, use Box<dyn Iterator<...>> or refactor the logic.
Mini Project
Description
Build a small Rust utility that extracts meaningful words from a line of text and lets the caller continue chaining iterator methods like take, filter, and count. This demonstrates how to return an iterator from a function in a practical text-processing scenario.
Goal
Create a function that returns an iterator over cleaned words from a string, then use it to count and collect selected words.
Requirements
- Write a function that accepts
&strinput and returns an iterator. - Ignore empty entries and surrounding whitespace.
- Convert words to lowercase before returning them.
- In
main, print the first three processed words. - In
main, also print how many processed words exist in total.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.