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:
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
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
(text)
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 (line: &) <Item = &> {
line.()
}
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: ) < <Item = &> + > {
trim {
::(text.())
} {
::(text.())
}
}
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
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 |
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) < <Item = &> + > {
::(text.())
}
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 ?
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
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Associated Types vs Generic Type Parameters in Rust: When to Use Each
Learn when to use associated types vs generic parameters in Rust traits, with clear rules, examples, and practical API design advice.
Convert an Integer to a String in Rust
Learn the current Rust way to convert integers to strings, why `to_str()` no longer works, and when to use `to_string()` or `format!`.