books-futures-explained icon indicating copy to clipboard operation
books-futures-explained copied to clipboard

Confusion on borrows across states

Open Banyc opened this issue 2 years ago • 3 comments

When I went thru the Pin things, I noticed that to_borrow: String is actually moved to the state enum. So, why not ditch all the unsafe stuff and borrow to_borrow directly?

cargo --version: 1.60.0

The code works for me but without unsafe code:

pub fn main() {
    let mut gen1 = GeneratorA::start();
    let mut gen2 = GeneratorA::start();

    if let GeneratorState::Yielded(n) = gen1.resume() {
        println!("Gen1 got value {}", n);
    }

    if let GeneratorState::Yielded(n) = gen2.resume() {
        println!("Gen2 got value {}", n);
    };

    let _ = gen1.resume();
    let _ = gen2.resume();
}

enum GeneratorState<Y, R> {
    Yielded(Y),
    Complete(R),
}

trait Generator {
    type Yield;
    type Return;
    fn resume(&mut self) -> GeneratorState<Self::Yield, Self::Return>;
}

enum GeneratorA {
    Enter,
    Yield1 { to_borrow: String },
    Exit,
}

impl GeneratorA {
    fn start() -> Self {
        GeneratorA::Enter
    }
}

impl Generator for GeneratorA {
    type Yield = usize;
    type Return = ();
    fn resume(&mut self) -> GeneratorState<Self::Yield, Self::Return> {
        let this = self;
        match this {
            GeneratorA::Enter => {
                let to_borrow = String::from("Hello");
                let borrowed = &to_borrow;
                let res = borrowed.len();
                *this = GeneratorA::Yield1 {
                    to_borrow,
                };

                GeneratorState::Yielded(res)
            }

            GeneratorA::Yield1 { to_borrow } => {
                let borrowed = to_borrow;
                println!("{} world", borrowed);
                *this = GeneratorA::Exit;
                GeneratorState::Complete(())
            }
            GeneratorA::Exit => panic!("Can't advance an exited generator!"),
        }
    }
}

Banyc avatar May 01 '22 17:05 Banyc

But the problem turns into the next state not being able to own the state value.

Banyc avatar May 01 '22 17:05 Banyc

Now I fix the next problem luckily. Still no unsafe code

pub fn main() {
    let mut gen1 = GeneratorA::start();
    let mut gen2 = GeneratorA::start();

    if let GeneratorState::Yielded(n) = gen1.resume() {
        println!("Gen1 got value {}", n);
    }

    if let GeneratorState::Yielded(n) = gen2.resume() {
        println!("Gen2 got value {}", n);
    };

    let _ = gen1.resume();
    let _ = gen2.resume();
}

enum GeneratorState<Y, R> {
    Yielded(Y),
    Complete(R),
}

trait Generator {
    type Yield;
    type Return;
    fn resume(&mut self) -> GeneratorState<Self::Yield, Self::Return>;
}

enum GeneratorA {
    Enter,
    Yield1 { to_borrow: Option<String>, to_own: Option<String> },
    Exit,
}

impl GeneratorA {
    fn start() -> Self {
        GeneratorA::Enter
    }
}

impl Generator for GeneratorA {
    type Yield = usize;
    type Return = ();
    fn resume(&mut self) -> GeneratorState<Self::Yield, Self::Return> {
        let this = self;
        match this {
            GeneratorA::Enter => {
                let to_borrow = String::from("Hello");
                let to_own = String::from("Hello but it is owned");
                let borrowed = &to_borrow;
                let res = borrowed.len();
                *this = GeneratorA::Yield1 {
                    to_borrow: Some(to_borrow),
                    to_own: Some(to_own),
                };

                GeneratorState::Yielded(res)
            }

            GeneratorA::Yield1 { to_borrow, to_own } => {
                let borrowed = &to_borrow.take().unwrap();
                println!("{} world", borrowed);
                drop(to_own.take().unwrap());
                *this = GeneratorA::Exit;
                GeneratorState::Complete(())
            }
            GeneratorA::Exit => panic!("Can't advance an exited generator!"),
        }
    }
}

Banyc avatar May 01 '22 17:05 Banyc

The example is contrived and showing a short example where you need a self referential struct is not easy (I tried but could not show a good example on the top of my head). One obvious point is that there is a performance penalty by unwrapping options (amongst others there is a branch that could panic in contrast to accessing a raw pointer) so there is a tradeoff already in your example. The generator Enum saves a minimal representation of the stack, and the stack can of course (and often do) contain references to other items on the stack.

If we change the example for our generator to this, I'm not sure your generator implementation would work:

let mut generator = move || {
        let to_borrow = String::from("Hello");
        let len = to_borrow.len();
        let mut borrowed = to_borrow.chars();
        print!("{}{}", borrowed.next().unwrap(), borrowed.next().unwrap());
        yield len;
        println!("{} world!", borrowed.take(3).collect::<String>());
    };

Other than that I can refer you to this article https://boats.gitlab.io/blog/post/2018-01-25-async-i-self-referential-structs/ and maybe this will lead you to some deeper understanding of why generators needs to be implemented as self-referential objects. If you do come to find a better explanation for it, please let me know!

cfsamson avatar May 04 '22 21:05 cfsamson