Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TryExtend trait #4664

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open

Conversation

bschoenmaeckers
Copy link
Contributor

This PR adds a TryExtend trait that is similar to the std::iter::Extend trait except it is failable. Currently implemented for PyList & PyDict, including some optimised specializations on nightly.

@@ -0,0 +1,23 @@
//! A trait for extending a collection with elements from an iterator, returning an error if the operation fails.
Copy link
Contributor Author

@bschoenmaeckers bschoenmaeckers Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find a obvious file to add this trait to so I created a new one. Suggestions are welcome.

@bschoenmaeckers bschoenmaeckers force-pushed the try-extend branch 4 times, most recently from 1a4ce61 to 3cd380f Compare October 28, 2024 17:29
@mejrs
Copy link
Member

mejrs commented Oct 28, 2024

I'm unsure whether I'm in favor of this, but I don't want specialization. We used to rely on it but removed it a couple of years ago. The problem with specialization (even the min version) is that it has soundness issues when it comes to lifetimes and it also has no realistic path towards stabilization.

Also, you can add a compatibility shim for PyList_Extend in the compat module.

@bschoenmaeckers
Copy link
Contributor Author

Alternatively I could add 2 methods to PyList & PyDict; extend_from_pyobject and extend_from_iter. This does not require specialization and is optimized for both cases.

Copy link

codspeed-hq bot commented Oct 29, 2024

CodSpeed Performance Report

Merging #4664 will not alter performance

Comparing bschoenmaeckers:try-extend (bc7cff0) with main (5464f16)

Summary

✅ 83 untouched benchmarks

@davidhewitt
Copy link
Member

Sorry to have been a little slow to get around to joining the discussion on this PR, so many parallel threads at the moment 😩

Thanks very much for working on our collections! I am 100% in agreement that there are things we could do to make PyO3's wrappers of Python collections interact better with Rust iterators. I have been wondering about whether we could implement both FromIterator and regular Extend for a while; I was wondering if we could support iterators of Bound<'py, T> for those traits. I'd love your take on those possibilities and how / why TryExtend is necessary instead.

I am a little cautious about TryExtend being a trait which seems to be much broader than just scoped to Python domain. I wonder, why is it that this isn't in something like itertools or even std? Should we be trying to upstream it, and then having itertools as an optional feature, maybe?

@bschoenmaeckers
Copy link
Contributor Author

bschoenmaeckers commented Nov 6, 2024

I have been wondering about whether we could implement both FromIterator and regular Extend for a while;

I was hesitant to implement non-failable traits because this may cause unexpected panics when python throws an error back when creating a python collection. That said I do not know all cases when python fails adding an item, so maybe this is not that big of a problem.

A Extend & FromIterator implementation for PyList could look like this,

impl<'py, T> Extend<Bound<'py, T>> for Bound<'_, PyList>
{
    fn extend<I>(&mut self, iter: I)
    where
        I: IntoIterator<Item = Bound<'py, T>>,
    {
        iter.into_iter().for_each(|item| {
            self.append(item).expect("Failed to append item to list");
        });
    }
}

impl<'py, T> FromIterator<Bound<'py, T>> for Bound<'py, PyList> {
    fn from_iter<I: IntoIterator<Item=Bound<'py, T>>>(iter: I) -> Self {
        let mut iter = iter.into_iter().peekable();
        let py = iter.peek().map(Bound::py)
            .expect("Cannot create a list from an empty iterator");
        let mut list = PyList::empty(py);
        list.extend(iter);
        list
    }
}

Furthermore the FromIterator implementation requires that the iterator is not empty so we can get a GIL token that lives long enough. This makes me question if we should even try to implement this trait.

I am a little cautious about TryExtend being a trait which seems to be much broader than just scoped to Python domain. I wonder, why is it that this isn't in something like itertools or even std? Should we be trying to upstream it, and then having itertools as an optional feature, maybe?

I like this idea, itertools has an ongoing discussion on fallible iterators so TryExtend maybe a good addition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants