How do I force a function lifetime parameter to be "shorter" than the impl lifetime parameter?


#1

Hello,

I have something like this:

impl<'a> TheStruct {
    fn stuff<'d : 'e, 'e>() {
        // do some setup stuff
        loop {
            // do some loopy stuff
        }
    }
}

2 questions - first of all, how can I force 'd to be “shorter” than 'a? With subtyping, I managed to make 'e shorter than 'd (because 'd : 'e guarantees that 'd will outlive, or match 'e, correct me if I’m wrong). How can I do the same for 'd and 'a (make 'd “shorter” than 'a)? The error message I’m getting is:

Second question is, right now, I’m trying to make 'e be the lifetime of the loop body (because I’m doing stuff in the loop that requires passing an explicit type parameter, for example, TypeTwo<'funclifetime, 'looplifetime>. So by using subtyping, I’m making sure that 'e is shorter than the function lifetime, which means that 'e SHOULD be the loop’s lifetime. Is that the case? If not, how can I explicitly get the named lifetime for the loop?

Thanks,
ndrewxie


#2

Actually what you want in the first question is 'a; ie from what I can tell you’re trying to return self's value which lives for 'a, and logically, the value returned should be the same lifetime as 'a, but can be coerced into a smaller lifetime. So the correct answer is to just use 'a. (If I understood your question properly.)
For the second one, I don’t exactly understand what you’re getting at; could you please elaborate?


#3

Any chance you can post the full code or at least a reduced example that would work in the playground?


#4

For the first one, basically, I want the lifetime of 'd to be shorter than 'a, always. For the second one, basically, I have a type (actually a type alias to a function), that takes 2 lifetime parameters. The first one is the lifetime for an input (which stays constant throughout program execution, but is determined at runtime, so it isn’t static - basically a “set once” input), and the second is the lifetime for the return type. The return type should only last for one loop iteration. Any way to force that 2nd lifetime ('e) to be the duration of the loop?


#5

Sure, the repo is link. The file in question is cellular_process.rs (it’s cellular_lib -> src -> cellular_automata -> cellular_process.rs. The directory structure is cancer, I know). The line range is 213 to 328. It’s hard to get a limited example, I’ve tried quite a few times before posting this (3, to be precise - each attempt took more than an hour. They either failed to replicate the issue or was way too long (100 lines or so)).


#6

you can add a where bound

impl<'a> TheStruct {
    fn stuff<'d : 'e, 'e>() where 'a: 'd {
        // do some setup stuff
        loop {
            // do some loopy stuff
        }
    }
}

#7

Thank you! What about the second part (getting a named / explicit lifetime for the loop to pass to a function). Right now, I’m making 'e a subtype of 'd, hoping that 'e would become the lifetime of the loop. Does that work?

Basically, I have a function:

fn the_func<'f, 'g>(input : &f) -> &'g [SomeDataType]

'g needs to be the lifetime of the loop (the returned reference should “expire” at the end of one loop iteration)


#8

Looking through the code you posted, do you need to use references? It looks like you are using them to store references to functions like &Fn(T) -> U, but you could just use normal function pointers, fn(T) -> U, if you don’t need to capture anything (which seems to be the case). Also, avoid putting references in structs, it really messes up the types and is really hard to work with, as it infects everything.


Also, you could rewrite stuff like access_flattened_vec to return an index, instead of accessing the slice directly.

pub fn coord_to_index(
    coords: &[isize],
    dimensions: &[usize],
    offset: usize,
) -> usize { /* your code here */ } 

Because std::cell::Ref implements Deref, &'_ Ref<'_, T> is redundant, instead just do &'_ T.

Similarly, &Vec<T> is useless, prefer a &[T]. Note &mut Vec<T> is not useless, because you can mutate the Vec's length.


Why are you using RefCell inside Grid? More importantly, why did you put a lifetime of Grid? It doesn’t look like anything is actually binding the lifetime, so why not remove the phantom that is making the lifetime?

If you have an internal function, you can mark it pub(crate) instead of pub, and it will only be visible inside the crate.


minor nit

  • in AdaptiveTransitionFunctionMapNode::Link you don’t need the Box
  • in GridDataType you have too many getters, just create 2
pub fn data(&self) -> &[CellDataType] {
    &self.data
}
pub fn data_mut(&mut self) -> &mut [CellDataType] {
    &mut self.data
}

If you need to turn it into a Vec, you can use the to_vec method on slices.

  • your new functions could be cleaned up
fn new(
        transition_1: &'a TransitionFunc<CellDataType, MetaDataType>,
        neighbor_parser_1: &'a NeighborFuncParser<'a, 'b, CellDataType, MetaDataType>,
        to_read_1: Ref<'b, GridDataType<CellDataType>>,
        meta_1: &'a MetaDataType,
    ) -> StaticExecArgs<'a, 'b, CellDataType, MetaDataType> {
        StaticExecArgs {
            transition: transition_1,
            neighbor_parser: neighbor_parser_1,
            to_read: to_read_1,
            meta: meta_1,
        }
    }

could be turned into

fn new(
        transition: &'a TransitionFunc<CellDataType, MetaDataType>,
        neighbor_parser: &'a NeighborFuncParser<'a, 'b, CellDataType, MetaDataType>,
        to_read: Ref<'b, GridDataType<CellDataType>>,
        meta: &'a MetaDataType,
    ) -> StaticExecArgs<'a, 'b, CellDataType, MetaDataType> {
        StaticExecArgs {
            transition,
            neighbor_parser,
            to_read,
            meta,
        }
    }

Instead of this

if let Ok(a) = tmp.try_borrow() {
    a
} else {
    panic!("Cannot borrow data");
}

You could simply do tmp.borrow(), which does the same thing. (Similarly for try_borrow_mut/borrow_mut). Although, removing the RefCells altogether is probably the best solution.


Looking back, I think I may have spent way too much time on this :sweat_smile:


#9

Thanks!


#10

To access the source, use this link instead, the line numbers are correct there.

Sorry to spend more of your time, but when I tried replacing fn with Fn, I’m getting expected fn pointer, found fn item. I’ve tried adding and removing borrows, does nothing to help. Yeah, I’mma remove the 'a for Grid, not sure why that’s there. But still, the same issue with 'e persists - the function mainloop_internal (line 228 in cellular_process.rs) is called once every single time in the loop starting at line 300, same file (I’ll try not to commit for the next day to avoid breaking the line numbers), so the reference returned by the function mainloop_internal has to ONLY live for the duration of that loop iteration. Any way to make the lifetime 'e reflect that? For that point, this is the problem:

Another manifestation of the same problem:

image


#11

Holey moley, that’s a lot of weird stuff you’re doing with lifetimes. Let’s clean this up.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e5a8c420332b3691f8e10f7d62b7c1db

  • I removed lifetime parameters from NeighborFuncParser by using for<'a, 'b> syntax
  • Now all places where lifetimes appear in StaticExecArgs are covariant, so it only needs one lifetime.
  • ExecFunc and RunFunc can lose their lifetimes similarly to NeighborFuncParser. (In this case, lifetime elision can fill in the for<'a> and etc., so I simply elided them)
  • The code does not use any unsafe (and the constructor takes no borrowed data), so there’s no reason for Grid to be carrying around a phantom lifetime. Perhaps you thought that by doing this, you were defining 'a as "the lifetime of the Grid"? That’s not how lifetimes work, however; adding a lifetime parameter to a type simply means that the compiler considers objects of that type to contain borrows with that lifetime. (adding lifetimes can only reduce the number of things that the borrow checker allows you to do, never increase it!)

This exposes the real error:

to_modify_data = Self::mainloop_internal(
    j * chunk_size,
    (j + 1) * chunk_size,
    &static_args,
    run,
    &mut to_modify_data,
); // note: function returns a slice of to_modify_data
   Compiling playground v0.0.1 (/playground)
error[E0506]: cannot assign to `to_modify_data` because it is borrowed
   --> src/lib.rs:574:13
    |
574 |                to_modify_data = Self::mainloop_internal(
    |   _____________^
    |  |_____________|
    | ||
575 | ||                 j * chunk_size,
576 | ||                 (j + 1) * chunk_size,
577 | ||                 &static_args,
578 | ||                 run,
579 | ||                 &mut to_modify_data,
    | ||                 ------------------- borrow of `to_modify_data` occurs here
580 | ||             );
    | ||             ^
    | ||_____________|
    | |______________assignment to borrowed `to_modify_data` occurs here
    |                borrow used here, in later iteration of loop

error: aborting due to previous error

Simplified further: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4a4c4d96fa72ce89aac9c10509700525

  • Did the name sugar mentioned by @KrishnaSannasi
  • Also replaced try_borrow as mentioned by the same.
  • Removed private new functions. If you need to construct something privately I see no reason not to use a struct literal.

#12

Oh wow, lmao. Thanks a lot! I’ve checked the official rust books, I didn’t really see things like “dyn for” and stuff, could you recommend somewhere I could learn about it and similar concepts?

Wow, the rust community’s really friendly. Back at some other communities, beginners like this who put questions like this without really minimal examples would be waved off immediately. Thanks @ExpHP and @KrishnaSannasi!


#13

It’s called a “Higher Ranked Trait Bound” (HRTB). It’s not in the book because use cases for them are pretty rare; the compiler is capable of automatically inserting the vast majority of them.


#14

Hmm… interesting. What’s stopping the compiler from inserting them in this case? Could the same be done (same fix) with “simpler” concepts?

Thanks a lot!


#15

The reason is because automatic for<'a> insertion obeys the same rules as function lifetime elision. If we fully expand a bunch of the Func type definitions, we get:

pub type TransitionFunc<CellDataType, MetaDataType> =
    dyn for<'a, 'b, 'c, 'd> Fn(
        &'a mut CellDataType,
        &'b NeighborType<'c, CellDataType>,
        &'d MetaDataType,
    ) -> ();

pub type NeighborFuncParser<CellDataType, MetaDataType /*: util::Newable*/> =
    dyn for<'a, 'b> Fn(
        &'b Ref<'b, GridDataType<CellDataType>>,
        &'a MetaDataType,
        usize,
    ) -> NeighborType<'b, CellDataType>;

pub type ExecFunc<CellDataType, MetaDataType /*: util::Newable*/> =
    dyn for<'a, 'b, 'c, 'd> Fn(
        &'a StaticExecArgs<'b, CellDataType, MetaDataType>,
        &'c mut DynamicExecArgs<'d, CellDataType>,
    ) -> ();

If you thought about similar functions, you get:

fn transition_func<'a, 'b, 'c, 'd, CellDataType, MetaDataType>(
    &'a mut CellDataType,
    &'b NeighborType<'c, CellDataType>,
    &'d MetaDataType,
) -> ();

fn neighbor_func_parser<'a, 'b, CellDataType, MetaDataType>(
    &'b Ref<'b, GridDataType<CellDataType>>,
    &'a MetaDataType,
    usize,
) -> NeighborType<'b, CellDataType>;

fn exec_func<'a, 'b, 'c, 'd, CellDataType, MetaDataType>(
    &'a StaticExecArgs<'b, CellDataType, MetaDataType>,
    &'c mut DynamicExecArgs<'d, CellDataType>,
) -> ();

According to the rules of lifetime elision, we can elide the lifetimes of exec_func and transition_func, but not for neighbor_func_parser (because it wouldn’t be clear whether NeighborType borrows from the Ref argument or the MetaDataType argument).

fn transition_func<CellDataType, MetaDataType>(
    &mut CellDataType,
    &NeighborType<CellDataType>,
    &MetaDataType,
) -> ();

fn neighbor_func_parser<'a, 'b, CellDataType, MetaDataType>(
    &'b Ref<'b, GridDataType<CellDataType>>,
    &'a MetaDataType,
    usize,
) -> NeighborType<'b, CellDataType>;

fn exec_func<CellDataType, MetaDataType>(
    &StaticExecArgs<CellDataType, MetaDataType>,
    &mut DynamicExecArgs<CellDataType>,
) -> ();

and so similarly, for<'a> elision† can happen automatically for TransitionFunc and ExecFunc, but not NeighborFuncParser.

†: (nobody actually calls it that)


#16

Thanks a lot again!

However, when I tried to create a function that has the same type as NeighborFuncParser, I’m running into this issue:

Any ideas?
The screenshot’s in a different file, you should be able to get all the details you need from the screenshot, but the file is here. The function neighbor_func_parser should have type NeighborFuncParser, so I just plugged in the type definition as the signature (with a few minor changes). I’ve tried changing the liftime of meta to 'c, but then the lifetime 'b isn’t used… but then the type of neighbor_func_parser no longer is NeighborFuncParser

This will be the last time I annoy ask you for help lmao, it’s probably getting annoying for you


#17

Okay, it’s hard for me to tell on my phone what type meta.get_neighbor_parser() returns (and therefore, the signature of its apply_neighbors), but I assume it looks like the signature of AdaptiveNeighborParser::apply_neighbors.

pub fn apply_neighbors<'a>(
    &self,
    grid: &'a Ref<'a, cellular_process::GridDataType<util::UsizeWrapper>>,
    meta: &'a AdaptiveMetaData,
    address: usize,
) -> cellular_process::NeighborType<'a, util::UsizeWrapper> {

The above signature says “the output may borrow data from grid and meta (but never self).”

pub fn apply_neighbors<'a, 'b>(
    &self,
    grid: &'a Ref<'a, cellular_process::GridDataType<util::UsizeWrapper>>,
    meta: &'b AdaptiveMetaData,
    address: usize,
) -> cellular_process::NeighborType<'a, util::UsizeWrapper> {

The above signature says “the output may borrow data from grid. (but never meta or self)” (P.S. I didn’t think of this before but the 'b lifetime can actually be elided)

Which is correct? You should adjust NeighborParserFunc and all apply_neighbors methods to match.


P.S.

impl<'a> AdaptiveNeighborParser {

This is a code smell. You have a lifetime in an impl which does not appear in the type (only in methods). Move it to the methods.


#18

Thanks a lot! That fixed the error!