Is std::iter::once really needed?

Looking at the source code of the (unstable) std::iter::Extend::extend_one, I noticed that Option<T> implements IntoIterator<Item = T> with the nameable type std::option::IntoIter as IntoIterator::IntoIter.

See source code of extend_one:

    /// Extends a collection with exactly one element.
    #[unstable(feature = "extend_one", issue = "72631")]
    fn extend_one(&mut self, item: A) {
        self.extend(Some(item));
    }

This means the following two approaches are somewhat identical:

fn main() {
    let mut option_iter: std::option::IntoIter<i32> = Some(5).into_iter();
    assert_eq!(option_iter.next(), Some(5));
    assert_eq!(option_iter.next(), None);
    
    let mut once_iter: std::iter::Once<i32> = std::iter::once(18);
    assert_eq!(once_iter.next(), Some(18));
    assert_eq!(once_iter.next(), None);
}

(Playground)

Does this mean that std::iter::once is superfluous? When to use which variant?

Well, extend(once(18)) is more readable than extend(Some(18)). That's probably the reason.

2 Likes

Also, iter::once() is an Iterator, whereas Option is not. This makes it (marginally) nicer when you immediately want to use it as an iterator, e.g. chain() something onto it. With Some(_), you'd need an explicit .into_iter() for that.

7 Likes

But why isn't once defined as follows then:

type Once<T> = std::option::IntoIter<T>;

fn once<T>(value: T) -> Once<T> {
    Some(value).into_iter()
}

fn main() {
    let mut iter: Once<i32> = once(5);
    assert_eq!(iter.next(), Some(5));
    assert_eq!(iter.next(), None);
}

(Playground)

This would make Once (as defined above) being the return value of once (as defined above) be identical with std::option::IntoIter.

Alternatively, Option could implement IntoIterator<IntoIter = std::iter::Once> instead of IntoIterator<IntoIter = std::option::IntoIter>.

In other words: Why are std::option::IntoIter and std::iter::Once two distinct data types? Don't they describe exactly the same?


In fact, std::iter::Once seems to be a newtype for std::option::IntoIter, see source:

/// An iterator that yields an element exactly once.
///
/// This `struct` is created by the [`once()`] function. See its documentation for more.
#[derive(Clone, Debug)]
#[stable(feature = "iter_once", since = "1.2.0")]
pub struct Once<T> {
    inner: crate::option::IntoIter<T>,
}

So the question is: why a newtype pattern here, and not a type alias?


To demonstrate the implications:

use std::iter::Once;
use std::option::IntoIter as AltOnce;

fn does_not_compile() {
    let _: AltOnce<()> = Some(()).into_iter(); // compiles
    let _: Once<()> = Some(()).into_iter(); // doesn't compile
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
 --> src/lib.rs:6:23
  |
6 |     let _: Once<()> = Some(()).into_iter(); // doesn't compile
  |            --------   ^^^^^^^^^^^^^^^^^^^^ expected struct `std::iter::Once`, found struct `std::option::IntoIter`
  |            |
  |            expected due to this
  |
  = note: expected struct `std::iter::Once<()>`
             found struct `std::option::IntoIter<()>`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error

Look at the line 65 here : once.rs - source

Once is (a newtype around) option::IntoIter.

3 Likes

Yes, that's what I meant with being a newtype (hiding the inner type).

But why isn't it a type alias instead?

A newtype is more flexible

Can you elaborate on this? Where does it provide more flexibility and where is that flexibility needed?

It forces me to decide which type to use. For example, is using OnceTwin or AltOnceTwin the better choice in the following example:

use std::iter::{Once, once};

use std::option::IntoIter as AltOnce;
fn alt_once<T>(value: T) -> AltOnce<T> {
    Some(value).into_iter()
}

pub struct OnceTwin {
    pub alpha: Once<i32>,
    pub beta: Once<i32>
}

impl OnceTwin {
    pub fn new(a: i32, b: i32) -> Self {
        OnceTwin {
            alpha: once(a),
            beta: once(b),
        }
    }
}

pub struct AltOnceTwin {
    pub alpha: AltOnce<i32>,
    pub beta: AltOnce<i32>,
}

impl AltOnceTwin {
    pub fn new(a: i32, b: i32) -> Self {
        AltOnceTwin {
            alpha: alt_once(a),
            beta: alt_once(b),
        }
    }
}

(Playground)

Once and AltOnce (as well as OnceTwin and AltOnceTwin) are incompatible (i.e. distinct) types. I don't see the value in that. They don't need to implement traits differently (I believe?).

It's more flexible from the point of view of the maintainers of the library. Because it doesn't expose the implementation.

2 Likes

Considering how fundamental the Option data type is, I wonder if the newtype could be replaced with a type alias to allow for more flexibility for the user of the library. Maybe that's a topic for IRLO though. I understand the motivation now.

Maybe. I don't know if it's worth it. Type aliases can have noticeably worse error messages from the compiler.

Member of libs-api here.

std::iter::once was added in Rust 1.2 while Option<T>::into_iter was included as part of Rust 1.0. You can see the RFC PR for std::iter::once here: Std::iter::once by XMPPwocky · Pull Request #771 · rust-lang/rfcs · GitHub

Indeed, the RFC itself says:

The existence of the Once struct is not technically necessary.

And there were people in the RFC discussion mentioning that Some(x).into_iter() already works. But it doesn't look like anyone felt terribly strongly about this at the time.

And here's the PR that landed std::iter::once itself: Implement std::iter::once/empty (RFC 771) by XMPPwocky · Pull Request #25817 · rust-lang/rust · GitHub

So now that the actual history is out of the way, I'll say something about the newtype. Looking at it with fresh eyes, I'm pretty certain we'd choose the same result today as folks did back in 2015. Namely:

  • The newtype approach is the "conservative" approach. If there was ever any difference in semantics or otherwise between Option<T>::into_iter and std::iter::once, then we'd have some flexibility to maneuver there because they aren't tied together at the API level. It's hard to think of what that difference could be, and I can't think of one presently. But we do tend to be quite conservative because std's API needs to last for approximately forever.
  • There's also a question of API sensibility here. Having a free function in the std::iter module return a type that is defined in the std::option module is... pretty weird.

In any case, as the RFC discussion mentioned, there are indeed multiple ways to achieve this. Much more than 2. For example, std::iter::repeat(wat).take(1). And more recently, the quite terse [wat].into_iter() now works too. With [wat] working, I do wonder if we still would have added std::iter::once. But either way, you have many options to choose.

Personally, if it were me, I'd use the operation that most closely matches your intent. That's probably std::iter::once.

Also, replacing the return type of std::iter::once with a type alias to a different type is almost certainly off the table. Probably on the grounds that there isn't sufficient motivation for such a thing. And it also feels like something that could be a breaking change, although I just woke up and can't pinpoint why exactly. (One possible reason is that the trait impls for the two types don't match exactly, but I'm not going to go through them and check.)

25 Likes

I agree that a once function is nice to have.

I guess (just talking hypothetically), that std::option could refer to std::iter though, in regard to iterator-related things, namely setting <Option as IntoIterator>::IntoIter = std::iter::Once. Then Once would need to be implemented differently though. But I see how this increases dependencies between the modules.

Language design aside (I understand it's staying as it is), …

So use std::iter::Once when it's about what I want to achieve and use std::option::IntoIter when it's about how it's achieved?

I'm not sure if that's the right phrasing. Maybe it is. But to add a little more clarity, I personally wouldn't use std::option::IntoIter unless I already had an Option<T> in hand for some other reason. It's same for something like std::vec::IntoIter. I don't use that unless I already have a Vec in hand. Think of Option<T> as a collection that contains at most 1 element, and into_iter() is how you iterate over that collection. Given some Option<T>, iterating over it may not produce 1 element, it might actually produce 0 elements.

1 Like

That's what I meant with using it when it's about "how" it's achieved (namely because I have already an Option<T> in hand.

Note though, that I can also construct a function that returns std::iter::Once<T> which might actually produce zero elements as well (it's a bit conceived contrived though):

fn never<T: Default>() -> std::iter::Once<T> {
    let mut iter = std::iter::once(Default::default());
    iter.next();
    iter
}

fn main() {
    let mut iter: std::iter::Once<String> = never();
    assert_eq!(iter.next(), None);
}

(Playground)

But I see that std::option::IntoIter could be used where this is not an unexpected/conceived contrived case.


Maybe I could/should see std::option::IntoIter as some sort of "EmptyOrOnce" (see std::iter::Empty).

I think you might mean contrived instead of conceived?

1 Like

Yes, thanks for correcting me.

This invalidates what I said here:

This would be awkward indeed. So I think I understand now why std::iter::Once and std::option::IntoIter are two distinct data types. They kinda serve a different purpose. (I guess.)

  • std::option::IntoIter is an iterator that returns one or zero items
  • std::iter::Once is an iterator that returns one item (though it will return zero items after next has been called once).

But would it then be more idiomatic to use once here:

As in:

     /// Extends a collection with exactly one element.
     #[unstable(feature = "extend_one", issue = "72631")]
     fn extend_one(&mut self, item: A) {
-        self.extend(Some(item));
+        self.extend(std::iter::once(item));
     }

Not that it really matters. Just asking to better understand code style / how to do it idiomatically. Maybe both choices are alright.

That would be a breaking change, because someone could have their own trait that they implement separately for each of those, which would become overlapping implementations if we aliased the types. I think that scenario is unlikely, but it's possible.

4 Likes

Okay, all those arguments make clear that those types won't be made the same type.

I still wonder when to use which, though, and if self.extend(Some(item)) or self.extend(once(item)) is more idiometic.

Regarding the types, I think I should use std::option::IntoIter if sometimes zero elements are emitted, otherwise std::iter::Once.


P.S.: I guess I just have to remember that there are often several ways in Rust to achieve the same thing. It's just a bit irritating sometimes.