Convert IntoIterator<Item = impl AsRef<str>> to &str and keep its lifetime in for loop

Hello friends, I hope you're doing well. I'm sharing the steps I tried, along with the errors I encountered, and the code snippets below. I would really appreciate it if you could help me understand this simple code.

In my function, I receive a list of strings as input from the user (like vec!["OXCTestHook"];). For example, since I don't know exactly how many entries there will be, I use the following parameter type:

names: impl IntoIterator<Item = impl AsRef<str>>,

In one part of the function, I need to loop through this list and apply an operation on each item one by one:

for name in names {
    let new_property = create_and_import_object_into_hook(name, allocator);
    obj_expr.properties.push(new_property);
}

The error I get at this part is:

rust-analyzer: expected &str, found String

So, I tried converting the names into Vec<String> before the loop to make sure I could pass &str to my function:

let names: Vec<String> = names.into_iter().map(|n| n.as_ref().to_string()).collect();
for name in names {
    let new_property = create_and_import_object_into_hook(&name, allocator);
    obj_expr.properties.push(new_property);
}

However, the error I got was:

rustc: `name` does not live long enough
borrowed value does not live long enough

Full error:

error[E0597]: `name` does not live long enough
   --> src/parser/ast.rs:188:71
    |
187 |             for name in names {
    |                 ---- binding `name` declared here
188 |                 let new_property = create_and_import_object_into_hook(&name, allocator);
    |                                                                       ^^^^^ borrowed value does not live long enough
189 |                 obj_expr.properties.push(new_property);
    |                 -------- borrow later used here
190 |             }
    |             - `name` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`

After that, I attempted converting each string to a reference. I don't know why ( :smiling_face_with_tear: :joy:), but I thought maybe this would solve the problem:

name.as_ref()
&name.as_ref()

Unfortunately, I kept getting the same repeated error:

rustc: `name` does not live long enough
borrowed value does not live long enough

I also tried random meaningless things like taking a reference and dereferencing it again (&*), hoping it might work.

Previously, with help from the community, I added this function to my project:

fn create_and_import_object_into_hook<'a>(
    name: &'a str,
    allocator: &Allocator,
) -> ObjectPropertyKind<'a> {

In that function, I stated that each name has a lifetime extending to the final output. However, I'm still getting the same errors.

These are all the steps I've tried and the errors I encountered. Sorry for the long post. I just wanted to see with your help what the problem might be that I can't execute such a simple loop here.

Thank you in advance.

The code is available at the following link:

Thank you in advance

This should work:

let names: Vec<String> = names.into_iter().map(|n| n.as_ref().to_string()).collect();
for name in &names {
    let new_property = create_and_import_object_into_hook(name, allocator);
    obj_expr.properties.push(new_property);
}

When you iterate over an owned Vec, it consumes the Vec and yields owned items, which means each String will be dropped at the end of each iteration. You want the Vec to keep ownership over the String items so your obj_expr can hold references to them after the loop is done. So instead, you iterate over &names, which yields &String, which coerce to &str when passed to create_and_import_object_into_hook.


If you're willing to change your function signature, then taking this might be better:

names: impl IntoIterator<Item = &impl AsRef<str>>,

This would accept Vec<&str> but not Vec<String>. However, you can once again pass &Vec<String> since that yields &String like above. This allows you to iterate over it directly without turning them all to String and collecting them first.

2 Likes

Hello, Unfortunately it does not work :smiling_face_with_tear:

Update

rustc: `names` does not live long enough
borrowed value does not live long enough

When I use this

let names: Vec<String> = names.into_iter().map(|n| n.as_ref().to_string()).collect();
for name in &names {
   let new_property = create_and_import_object_into_hook(name, allocator);
   obj_expr.properties.push(new_property);
}

here is a short code for the question.


in the for loop, name would be dropped at the end of loop scope, then lifetime error occurs.
I see name is used by program by reference, so, at least name shoud live longer than program


  • if you don't want to change function signature, use a collect for names, but would have cost for memory allocation
  • if you can change function signature, use a reference way for names maybe better
1 Like

Ah, you can move let names into a higher scope. I'm not sure exactly how high it needs to go, but the top of the function is definitely sufficient.

pub fn extend_hook_object_to_ast(
    file_path: &str,
    names: impl IntoIterator<Item = impl AsRef<str>>,
    allocator: &Allocator,
) -> Result<String, String> {
    let names2: Vec<String>;                                        \\ <- here
    let mut program = source_to_ast(file_path, allocator)?;

    if !has_live_socket(&program) {
        return Err("liveSocket not found.".to_string());
    }

    let maybe_properties = get_properties(&mut program.body);
    if let Some(properties) = maybe_properties {
        let hooks_property = match find_hooks_property(properties) {
            Some(prop) => prop,
            None => {
                let new_hooks_property = create_init_hooks(allocator);

                properties.push(new_hooks_property);
                get_property_by_key(properties.last_mut().unwrap(), "hooks").unwrap()
            }
        };

        if let Expression::ObjectExpression(obj_expr) = hooks_property {
            // let new_property = create_and_import_object_into_hook(name, allocator);
            // obj_expr.properties.push(new_property);

            // use the outer one
            names2 = names.into_iter().map(|n| n.as_ref().to_string()).collect(); 
            for name in &names2 {
                let new_property = create_and_import_object_into_hook(name, allocator);
                obj_expr.properties.push(new_property);
            }
        }
    } else {
        return Err("properties not found in the AST".to_string());
    }

    let codegen = Codegen::new();
    let generated_code = codegen.build(&program).code;
    Ok(generated_code)
}

If you instead change the signature, this should work:

pub fn extend_hook_object_to_ast(
    file_path: &str,
    names: impl IntoIterator<Item = &impl AsRef<str>>,
    allocator: &Allocator,
) -> Result<String, String> {
    let mut program = source_to_ast(file_path, allocator)?;

    if !has_live_socket(&program) {
        return Err("liveSocket not found.".to_string());
    }

    let maybe_properties = get_properties(&mut program.body);
    if let Some(properties) = maybe_properties {
        let hooks_property = match find_hooks_property(properties) {
            Some(prop) => prop,
            None => {
                let new_hooks_property = create_init_hooks(allocator);

                properties.push(new_hooks_property);
                get_property_by_key(properties.last_mut().unwrap(), "hooks").unwrap()
            }
        };

        if let Expression::ObjectExpression(obj_expr) = hooks_property {
            // let new_property = create_and_import_object_into_hook(name, allocator);
            // obj_expr.properties.push(new_property);

            for name in names {
                let new_property = create_and_import_object_into_hook(name.as_ref(), allocator);
                obj_expr.properties.push(new_property);
            }
        }
    } else {
        return Err("properties not found in the AST".to_string());
    }

    let codegen = Codegen::new();
    let generated_code = codegen.build(&program).code;
    Ok(generated_code)
}
1 Like

Thank you, the second one I think needs lifetime

 names: impl IntoIterator<Item = &impl AsRef<str>>,
    |                                      ^ expected named lifetime parameter

How could you recognize the first way(outer var) can fix the problem?

Ahhh, I’m a bit confused. In the example you provided, there’s a lot of lifetimes and types involved. I’ll try to implement it in my code.

You mentioned at one point:

Is this example you provided the best approach you are suggesting?

The error you were getting is because obj_expr_properties is from an outer scope. I can't tell from looking at just this function for sure, but it appears to be a reference to a part of program, which needs to survive to codegen.build(&program). So the Vec needs to be dropped after that, and declaring it before program will do that.

wangbyby's code has the fix for the lifetime problem, although now that I thought some more about it, it could be better to just take Item = &'a str. That's slightly more work for the caller (they might have to .map(|s| s.as_ref())), but it shouldn't make it less possible to call the function.

Playground of the &'a str method

2 Likes

maybe there have a better way for lifetime in function signature.


In my own project, I use String and clone :joy:.
&'a str is tough

1 Like

It works perfectly

What way you suggest for this place if you were as a rust developer (in this place), and way?

Sorry, I just want to learn more from you friends.
Thank you in advance

I'll compare these two options. The main thing is that 1 is easier to read, while 2 is easier to call.

Signature 1:

fn test<'a>(names: impl IntoIterator<Item = &'a str>)

This is easily readable. If people need to refer to the documentation of this function, they'll be quick to understand what it is and how to call it.

Call 1:

// names: Vec<String>
test(names.iter().map(|s| s.as_ref()));

This is how you would call Signature 1 if you have a value of type Vec<String>. It's a bit verbose but not fundamentally restrictive, in that any iterator that should work, can be adapted to work. If you use this function a lot, this might get annoying to type all the time.

Signature 2:

fn test<'a>(names: impl IntoIterator<Item = &'a (impl AsRef<str> + 'a + ?Sized)>)

This looks complex even though it's not that different for the caller. It might require explanation, in docs or elsewhere.

Call 2:

// names: Vec<String>
test(&names);

Much fewer characters and less noisy than Call 1.

There isn't any difference in allocation or other big performance concerns, and under ideal conditions they should compile to the same machine code. It's just a matter of dev experience.

2 Likes

impl AsRef<str> isn't required to be &str, it can be String or ArrayString or something else that owns the string data, and is owned by the iterator, so it gets destroyed when the iteration moves to the next element, and you don't save it.

You can't keep name.as_ref() for any longer than you keep name for. If name is destroyed, the reference to its &str will be destroyed too.

.as_ref() doesn't give you a long-lived reference. It gives you a short-lived temporary view into the variable, valid only in the scope of the variable.

Additionally, you can't keep name.as_ref() result if the name is moved, because moves invalidate any references to the object. This means you can't take a reference and then store name in the same pass.

If you receive impl IntoIterator<Item = impl AsRef<str>>, then in typical cases you won't be able to put these references in anything like Vec<&str>, because iterating through the iterator moves and destroys the strings as you iterate them. You would have to first give longer-lived storage to the items of the iterator, something like Vec<impl AsRef<str>>, and then be able to borrow &str from that Vec, but that makes use of IntoIterator pointless, and it'd be better to ask for &[impl AsRef<str>].

Don't ask for owning IntoIterator of impl AsRef<str> if you need &str that survives longer than local scope of processing of only one element at a time.

&impl AsRef<str> gets complicated, and has all of the restrictions of &str, so it's better to just ask for impl IntoIterator<Item = &'long str>.

3 Likes

As a side note, &impl AsRef<str> can't be a &str. You probably intended something along the lines of a &(impl AsRef<str> + ?Sized).

(Just a side note as I agree you should just use &str instead.)

1 Like

Thank you and all of the friends who helped me :heart::heart:
I used this names: impl IntoIterator<Item = &'a str>, in my code

and The loop part

if let Expression::ObjectExpression(obj_expr) = hooks_property {
    for name in names {
       let new_property = create_and_import_object_into_hook(name.as_ref(), allocator);
       obj_expr.properties.push(new_property)
    }
}