Thoughts about Iterators - can we have move-or-mutate semantic for functions?


#1

Ignoring those methods with default implementation, a iterator is just

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

The problem of the above design, is that it is not able to limit the implementation from returning Some after None being returned. So we have FusedIterator and fuse as workaround.

But at least in theory, the Iterator trait should looks like

pub trait Iterator {
    type Item;
    fn next(self) -> Option<(Self,Self::Item)>;
}

With this, once it returns None no one can use the iterator any more as it is already moved out. If it returns Some, you were given back the object itself, so you can continue iterating. Perfect!

However, this design does not work for today’s Rust. The biggest problem is that it makes Iterator not object safe. The second problem is passing self by value prevents sending trait objects.

But in my mind, this API is just the same as the old one, up to some de-sugaring. So, are there any ideas to make the later API de-sugar to the API (but keep the addition restriction to implementation) above without affecting too much?


#2

Aren’t these statements contradictory?


#3

Well, when I talked about the restrictions I mean in practice they are different, and when I say they are just the same, I mean they are semantically the same, i.e. means the same thing to the programmer, and the difference are just about how it is implemented.


#4

The reason I am interested in this, is that I am designing a “Item consumer” trait, the natural interface will be the following:

trait ItemConsumer<T> {
    fn consume(self, T) -> Result<Self, T>;
}

With this interface, the implementation will accept an input of type T, returning Ok(Self), or reject the input, returning Err(T). If the input was accepted, it was used to modify the internal status of self, so the input is gone but the mutated self will be returned. If it is rejected, the object is no longer usable so it is gone, but the input will not be consumed and will be returned.

Unfortunately the above have the same problem with the alternative of Iterator: it is not object safe.

So, basically the problem is the current Rust language does not support conditional move or mutate semantic. I am not able to express a function that can make a choice to consume its argument, or simply just mutate it.


#5

One proposal for the above, is to allow writing

trait ItemConsumer<T> {
     fn consume(&mov self, &mov T);
}

implementations can then write

impl ItemConsumer for MyType {
    fn consume(&mov self, t: &mov T){
         if condition {
             move self;
         } else {
             move t;
         }
    }
}

The above desugar to the normal &mut form, but the return type have some additional type:


enum MovingStatus<'a, T> {
    Mutated(&'a mut T),
   Moved
}

trait ItemConsumer<'a,'b,T> {
    fn consume(&'a mut self, &'b mut T) -> (MoveStatus<'a, Self>, MoveStatus<'b, T>,());
}

impl trait ItemStatus<'a,'b,T> for MyType {
    fn consume(&'a mut self, t: &'b mut T) -> (MoveStatus<'a, Self>, MoveStatus<'b, T>,()){
        if condition {
            (Mutated(self),Moved,())
        } else {
           (Moved, Mutated(t), ())
        }
    }
}

The following calling site code

   let mut consumer:ItemComsumer<i32> = ...
   ...
   let mut i= 10;
   consumer.consume(i);

desugar to

   let mut consumer:ItemComsumer<i32> = ...
   ...
   let mut i= 10;
   match consumer.consume(i) {
       (Moved,Moved,_) => { drop(consumer); drop(i) },
      (Moved,_,_) => { drop(consumer); }
      (_,Moved,_) => { drop(i); }
      _ => {} 
   }

As all the above are just syntactic sugar, and the desugared trait is object safe, the original trait should be object safe as well.

The above code assumes all the values are gone at the same time. We will need more syntactic structure to allow the calling site to check whether the values are alive.


#6

You can provide an object-safe wrapper trait, and then provide a blanket impl for it. Playground link

trait BoxableItemConsumer<T> {
    fn boxed_consume(self: Box<Self>, t: T) -> Result<Box<BoxableItemConsumer<T>>, T>;
}

impl<T, U: ItemConsumer<T> + 'static> BoxableItemConsumer<T> for U {
    fn boxed_consume(self: Box<Self>, t: T) -> Result<Box<BoxableItemConsumer<T>>, T> {
        self.consume(t).map(|x| Box::new(x) as _)
    }
}