Generics implementing PartialEq not general enough, must outlive 'static lifetime

I am running into an issue trying to make a function more generic. In its simplest form, I am trying to take this function that sees if two values are equal:

fn works<'l>(l: Option<&'l str>, r: Option<String>) -> bool {
    if l != r.as_ref().map(String::as_str) {
        false
    } else {
        true
    }
}

and make it more generic so the l value can be anything that can do an equality test against r:

fn does_not_work<'l, L: 'l + for<'r> std::cmp::PartialEq<std::option::Option<&'r str>>>(
    l: L,
    r: Option<String>,
) -> bool {
    if l != r.as_ref().map(String::as_str) {
        false
    } else {
        true
    }
}

but I end up getting this error message:

error[E0521]: borrowed data escapes outside of function
  --> src/main.rs:9:5
   |
7  | fn run_test<'l>(l: &'l str, r: Option<String>) -> () {
   |             --  - `l` is a reference that is only valid in the function body
   |             |
   |             lifetime `'l` defined here
8  |     // works(Some(l), r);
9  |     does_not_work(Some(l), r);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^
   |     |
   |     `l` escapes the function body here
   |     argument requires that `'l` must outlive `'static`
   |
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
  --> src/main.rs:20:30
   |
20 | fn does_not_work<'l, L: 'l + for<'r> std::cmp::PartialEq<std::option::Option<&'r str>>>(
   |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: implementation of `PartialEq` is not general enough
 --> src/main.rs:9:5
  |
9 |     does_not_work(Some(l), r);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `PartialEq` is not general enough
  |
  = note: `Option<&'2 str>` must implement `PartialEq<Option<&'1 str>>`, for any lifetime `'1`...
  = note: ...but it actually implements `PartialEq`, for some specific lifetime `'2`

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

Does anyone know what I am doing wrong?

Full test code:

fn main() {
    let r = Some("foo".to_owned());
    let l = "foo";
    run_test(l, r);
}

fn run_test<'l>(l: &'l str, r: Option<String>) -> () {
    // works(Some(l), r);
    does_not_work(Some(l), r);
}

fn works<'l>(l: Option<&'l str>, r: Option<String>) -> bool {
    if l != r.as_ref().map(String::as_str) {
        false
    } else {
        true
    }
}

fn does_not_work<'l, L: 'l + for<'r> std::cmp::PartialEq<std::option::Option<&'r str>>>(
    l: L,
    r: Option<String>,
) -> bool {
    if l != r.as_ref().map(String::as_str) {
        false
    } else {
        true
    }
}

The error message says it explicitly:

note: due to current limitations in the borrow checker, this implies a `'static` lifetime
  --> src/main.rs:16:21
   |
16 | fn does_not_work<L: for<'r> PartialEq<Option<&'r str>>>(
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

so this is not possible.

Do you have a concrete reason for doing so or are you being generic for the sake of being generic? In particular, why do you need to support non-Options for the l parameter?

If you don't, this or something similar[1] seems like a better fit to me.

fn also_works<L: AsRef<str>>(
    l: Option<L>,
    r: Option<String>,
) -> bool {
    l.as_ref().map(|s| s.as_ref()) == r.as_deref()
}

  1. why take these by value? ↩ī¸Ž

2 Likes

Ah that will work! I had made the attempt to use an Option<L> without doing any AsRef<str> stuff, which did not work, but I absolutely can make sure my L's implement AsRef<str>.

The rest of this post is just sharing the full context to satisfy any curiosity, but there are no open questions left:

The full context is I'm working on an org-mode parser and I'm working on the code that compares the AST from my parser (L) vs the upstream emacs org-mode AST (R).

left: Org-mode likes to do this thing where sometimes (but not always!) consecutive whitespace becomes a single space ("foo \t\nbar" becomes "foo bar") so I sometimes have an Option<&str> if I don't need to coalesce the whitespace, or an Option<String> if I did need to.

right: The text I am getting out of the emacs AST is quoted text, as in the value contains the quotation marks and can have escaped characters ("fo\"o" becomes fo"o), so unescaping the text and removing the surrounding quotes is how I end up with an Option<String> on the right.

The reason I couldn't pre-process it more is I am defining these checks using a macro that takes in a list of all expected properties and the functions to compare the values, so I can assert that all properties are compared and no unexpected properties pop up in the emacs AST. For example:

compare_properties!(
        emacs_ast_node,
        rust_ast_node,
        (
            EmacsField::Required(":path"), // <--- defines field in emacs AST
            |rust_ast_node| Some(rust_ast_node.get_path()), // <--- A getter to read the same field from the rust AST
            compare_property_quoted_string // <--- The function that compares the two
        ),
        (
            EmacsField::Required(":format"),
            |_| Some("bracket"),
            compare_property_unquoted_atom
        )
    )?

So why can't I pre-process the left side to get it to an Option<&str>? The reason is the middle parameter in that list is a getting to read the value out of the rust_ast_node so if I defined it as:

|rust_ast_node| {
    let value: Option<String> = rust_ast_node.get_value().map(coalesce_whitespace);
    value.as_ref().map(String::as_str)
}

Then it would be returrning a borrowed value from the owned Option<String> inside that function, which ofc is not allowed.

So before your help I had to define two compare functions to handle the two possible types from my rust AST (based on whether or not I had to coalesce adjacent whitespace): compare_property_quoted_string (which handled Option<&str>) and compare_property_quoted_string_owned (which handled Option<String>).

So why am I passing in a getter-function instead of just directly reading the rust_ast_node and passing the value into the macro? For example:

let rust_path: Option<String> = rust_ast_node.get_path().map(coalesce_whitespace);
compare_properties!(
        emacs_ast_node,
        (
            EmacsField::Required(":path"), // <--- defines field in emacs AST
            rust_path.as_ref().map(String::as_ref),
            compare_property_quoted_string // <--- The function that compares the two
        )
)?

That would solve the issue with returning a borrow of an owned value from the getter, but I am hoping to treat this list of fields and their compare functions as static config data divorced from the actual AST nodes. Maybe one day I'll return this list of config data from a struct instead of hard-coding it into my compare_* functions.

PS: Based on your name, I'm assuming you're the author behind this. I cannot thank you enough! I had that yellow-flag pattern all over my code base causing lifetime issues that went away when I fixed them. Also your article of variance explained why one of my previous attempts at an Org-Mode parser suddenly fell into inexplicable lifetime issues when I switched to passing around FnMut. Sadly I discovered that article years too late, but it feels great to finally have closure.

It sounds like you could profit from Cow, have you tried that?

Yeah, I actually just added my first use of it recently because I needed to handle link templates, but you are right: there are a lot more places where I should be using Cow. For example, the unquoting of text I described above could just be a slice operation if the text doesn't have any escaped characters but I am currently naively dumping it into a String regardless. I'm going to have to revisit my heap allocations when I'm optimizing this. Thanks for the suggestion!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.