Lifetime notation: type instance reference, in a Box, in an Option

I need another hand to fully understand lifetime notation, and its proper specification.

Quick summary: I'm building a potentially elaborate nested directed graph of hook instances. Each hook will execute some atomic transformation of the workpiece. In this example the workpiece is a string, but my problem is with lifetime notation of a factory helper function.

:white_check_mark: since the references are chained, and Rust hates null references, we have an Option.

:white_check_mark: since the references are among self-referential types, we have the reference in a Box — I'm guessing ultimately this will be a dyn Box, but I'm not there yet.

:white_check_mark: I can assemble an arbitrary-length hook chain explicitly, no problem there.

:x: My problem is, when I create a function to automate appending a hook on the end of the hook chain, Rust complains about lifetimes. I have tried putting the lifetime notation in ALL possible positions, using all the confections my noob skill knows, and I can't get this to compile. This fails on line 24 — shown here without any failed lifetime-tic notation attempts, so someone can please steer me right.

:roll_eyes: Also note, the error message also says, expected 'potato' \n found 'potato'. Is that a known compiler bug, or is this legit because of potentially several problems that lie beyond this lifetime notation blocker?

Thank you for your patience with me :smile_cat:

// Experimenting with hooks in Rust

use std::option::Option;

#[derive(Debug, Clone)]
pub struct Hook<'a> {
    pub hook: Option<Box<&'a Hook<'a>>>,
}

pub trait Hooking {
    type Thing;
    fn sethook<'a>(&self, t: &'a mut Self) -> &'a mut Self;
}

impl Hooking for Hook<'_> {
    type Thing = String;

    fn sethook<'a>(&self, hook_passed: &'a mut Self) -> &'a mut Self {
        match &self.hook {
            Some(h) => {
                h.sethook(hook_passed);
            }
            None => {
                self.hook = Some(Box::new(hook_passed));
            }
        }
        hook_passed
    }
}

fn main() {
    let mut h1 = Hook {
        hook: None,
    };
    let h2 = Hook {
        hook: None,
    };
    h1.sethook(&mut h2);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
  --> src/main.rs:24:34
   |
24 |                 self.hook = Some(Box::new(hook_passed));
   |                                  ^^^^^^^^^^^^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the lifetime `'a` as defined on the method body at 18:16...
  --> src/main.rs:18:16
   |
18 |     fn sethook<'a>(&self, hook_passed: &'a mut Self) -> &'a mut Self {
   |                ^^
note: ...so that reference does not outlive borrowed content
  --> src/main.rs:24:43
   |
24 |                 self.hook = Some(Box::new(hook_passed));
   |                                           ^^^^^^^^^^^
note: but, the lifetime must be valid for the lifetime `'_` as defined on the impl at 15:23...
  --> src/main.rs:15:23
   |
15 | impl Hooking for Hook<'_> {
   |                       ^^
note: ...so that the expression is assignable
  --> src/main.rs:24:29
   |
24 |                 self.hook = Some(Box::new(hook_passed));
   |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = note: expected  `std::option::Option<std::boxed::Box<&Hook<'_>>>`
              found  `std::option::Option<std::boxed::Box<&Hook<'_>>>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0495`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

Box is already a pointer, you don't need Box<&'a Hooj<'a>>, do either

struct Hook {
    hook: Option<Box<Hook>>
}
// or
struct Hook<'a> {
    hook: Option<&'a Hook<'a>>
}

I'll assume the latter, which get's you something like this

pub trait Hooking<'a> {
    type Thing;
    fn sethook(&self, t: &'a Self);
}

impl<'a> Hooking<'a> for Hook<'a> {
    type Thing = String;

    fn sethook(&self, hook_passed: &'a Self) {
        match self.hook {
            Some(h) => h.sethook(hook_passed),
            None => {
                self.hook = Some(hook_passed);
            }
        }
    }
}

Note how the lifetimes match up, this is what allows Rust to see that there can be no dangling references!

2 Likes

Thank you for freely speculating @RustyYato. Your suggestion doesn't work.

My question is, how does one put a lifetime on an expression that's a reference inside box, inside an option. I can't even determine where, in Line 24, Rust will accept lifetime notations.

Note: there's a link to the playground in my question.

Sorry about that, here's a working version (playground)

struct Hook<'a> {
    hook: Option<&'a mut Hook<'a>>
}

pub trait Hooking<'a> {
    type Thing;
    fn sethook(&mut self, t: &'a mut Self);
}

impl<'a> Hooking<'a> for Hook<'a> { // this 'a on Hook is important
    type Thing = String;

    fn sethook(&mut self, hook_passed: &'a mut Self) {
        match self.hook {
            Some(ref mut h) => h.sethook(hook_passed),
            None => {
                self.hook = Some(hook_passed); // hook_passed : &'a mut Self 
                                               // self.hook   : &'a mut Self
            }
        }
    }
}
1 Like

Thanks @RustyYato, that playground works!

I'm having a heck of a time transposing this stripped-down mock back into my more elaborate program. (Sigh!).

Thanks for your help. It sends me in a new direction that feels as much of an impasse as the original direction.

How is a self-referential chained structure, whose extent is unknown at compile time, even possible without a Box?

I would avoid self-referential types all together, they are much harder to work with in Rust. It looks like you are trying to create a linked-list of sorts, and anyone doing that must read https://rust-unofficial.github.io/too-many-lists/ :slight_smile:. One way around this is to use reference counting.

something like this: playground

use core::cell::Cell;
use std::rc::Rc;

pub struct Hook {
    pub hook: Cell<Option<Rc<Hook>>>,
}

pub trait Hooking {
    fn sethook(&self, t: Rc<Self>);
}

impl Hooking for Hook {
    fn sethook(&self, hook_passed: Rc<Self>) {
        match self.hook.take() {
            Some(h) => {
                self.hook.set(Some(h.clone()));
                h.sethook(hook_passed);
            },
            None => {
                self.hook.set(Some(hook_passed));
            }
        }
    }
}
2 Likes

@RustyYato I'm trying to replicate something I've written many times before: a mutable chain of responsibility, with each hook responsible for a simple, testable mutation of the workpiece.

It's generally a bad ideaTM to put lifetimes on a struct. Lifetime annotations are really well explained in @jonhoo's video Crust of Rust: Lifetime Annotations

edit: Long story short, in the following type,

struct Foo<'a> { foo: Option<&'a Foo> }

'a refers to the lifetime of some variable of type Foo that already exists somewhere else. The lifetime annotations just says, here is that lifetime, it is a description of your program, nothing more, nothing less.

Ok, that's awesome, but you may have to rethink how you approach the problem. Rust is significantly different from other languages, so please leave any baggage behind. Many patterns that work beautifully in other languages don't translate well to Rust.

(Discorse why did you make me edit my last post instead of making a new post?!?!?)

2 Likes

Thanks for that explanation @RustyYato. It's just that, on the surface, Rust appears ideal for this chain of responsibility scenario. In Go, given its lack of generics, it's an avalanche of repeated boilerplate code. In other languages, it's doable, easy in fact. I'll be dissappointed if I can't crack this with Rust.

You can't ascribe some lifetime annotations as written, because it is incorrect

  1. You can't store a &mut T and return it.
    a. &mut T means exclusive reference, and storing and return would create aliasing exclusive references, which is absurd
  2. playground here is the smallest diff I can come up with to make it compile, but the box is not needed, as seen here
  3. If you want shared references + mutation, then you want &Cell<Hook> or &Mutex<Hook> depending on your threading requirements instead of &mut Hook

edit: If you really want to return a reference to the inner Hook, then you can do something like this (notice the difference in the lifetime annotations)

2 Likes

@RustyYato Here's the GitHub for this.

Works perfectly. I can create Hooks A, B, and C and thereafter I can say A.hook = B; and B.hook = C; and it's perfect. I have a hook chain A - B - C and they fire in order and it post-processes back, in reverse order. Beautiful.

All I want to do is, feed the next hook (say D) to A (the head of the chain) and have it travel along the hook chain until it finds None at the end of the chain and thus appends it, and we get A - B - C - D.

I can't believe this can't be done, in a simple straightforward way, in Rust.

[Moderator note: Hid a few comments. If this discussion continues, it needs to be much, much more chill.]

2 Likes

I gave you a solution in my previous post, try and integrate it into your code.

2 Likes

The benefits of Rust come with costs, and one of the biggest costs is that some data structures are much trickier (in some cases even impossible) to implement efficiently in safe code. It's not because Rust wants to make your life hard, but because of the inherent difficulty of preserving memory safety without pervasive garbage collection. The hard parts of Rust do get somewhat easier with experience, though it remains more restrictive by design than many other languages.

4 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.