Creating an empty iterator of a certain type in Rust (2018)

airstrike | 35 points

I stumbled upon this article today. My first thought was that this is an interesting question, then I continued reading and thought that the solution ideas are kind of crude. Then I noticed it was written at 2018, so I realized we should give the author a break. Finally I realized that it was me who wrote this, 6 years ago!

So much has changed, but I believe the easy solution already existed back then. As people wrote in the comments here, one can iterate over an Option and then use flatten.

Strange experience indeed.

realcr | 4 months ago

Because `OptionIterator` implements the `Iterator` trait, the function signature can be simplified to return just `impl Iterator<…>` like the original version did, leaving the additional type as an implementation detail.

Also, a similar solution is provided by the `either` crate [1], where the function body can return `Either::Left(std::iter::empty())` in one case and `Either::Right(…)` in the other, also hiding all of this behind an `impl Iterator<…>` return type.

[1] https://docs.rs/either/latest/either/enum.Either.html

kd5bjo | 4 months ago

You don't need to do all that much, you can use `Option` with `into_iter` paired with `flatten` instead:

    fn neighbors_with_send_capacity(&self, a: N, capacity: u128) -> impl Iterator<Item = &N> + '_ {
        self.nodes
            .get(&a)
            .map(|m| m.keys())
            .into_iter()
            .flatten()
            .filter(move |b| self.get_send_capacity(&a, b) >= capacity)
    }
The only downside is that I believe `size_hint` is not properly forwarded.
the_mitsuhiko | 4 months ago

Another possible solution for this use case: let each capacity graph store a “dummy” `HashMap<N,(u128, u128)>` for the case where the key is missing. Then, return an iterator over the empty dummy to get an iterator of the same type.

I will however note that simply using an option in the return value is probably the best choice both API- and performance-wise. The consumer may want to do different things if the key doesn’t exist versus if the key has no neighbors. Additionally, returning an Option means there’s only one check for whether the retrieval was correct, rather than checking at every call to `next`.

claytonwramsey | 4 months ago

There is another alternative which may be significantly more efficient as it means the caller always gets back a direct iterator instead of an enum it must branch on: make a dummy empty hashmap with a static lifetime you can refer to. Then the implementation becomes

    static DUMMY: LazyLock<HashMap<N, (u128, u128)>> = LazyLock::new(HashMap::new());

    fn neighbors_with_send_capacity_option(&self, a: N, capacity: u128) -> impl Iterator<Item=&N> {
        let node = self.nodes.get(&a).unwrap_or_else(|| &*DUMMY);
        node.keys().filter(move |b| self.get_send_capacity(&a, b) >= capacity)
    }
Unfortunately we have to use a LazyLock for the static because HashMap::new isn't const. It only adds a single always-correctly-predicted branch that checks for initialization on access though, so it's fine.
orlp | 4 months ago

> However, this solution is not very ergonomic, as it requires the user of the function to first check if the returned value is Some(iterator) and only then iterate over the iterator.

I'd use `flatten()` to iterate over this nested type.

kawogi | 4 months ago

fwiw, Option already implements iterator (well, IntoIterator) - calling .into_iter() on an Option will get you an iterator, no need to create anything custom.

Patryk27 | 4 months ago

I don't understand the people who look at this and go "yep, we should have more of this language". It's complexity for its own sake.

sk11001 | 4 months ago

Performance or not, this language is borderline unreadable.

blumenkraft | 4 months ago