clap icon indicating copy to clipboard operation
clap copied to clipboard

clap 4.3: Implementation of `FnOnce` is not general enough

Open lukesneeringer opened this issue 2 years ago • 5 comments

Please complete the following tasks

Rust Version

1.69.0

Clap Version

4.3.0

Minimal reproducible code

use anyhow::Result;
use clap::Parser;

#[derive(Clone, Debug)]
struct Offset {}

impl Offset {
    pub fn parse(s: impl AsRef<str>) -> Result<Self> {
        Ok(Self {})
    }
}

#[derive(Clone, Debug, Parser)]
struct Command {
    #[arg(long, value_parser = Offset::parse)]
    pub offset: Offset,
}

fn main() {
    println!("Hello, world!");
}

Steps to reproduce the bug with the above code

cargo build

Actual Behaviour

error: implementation of `FnOnce` is not general enough
  --> src/main.rs:13:24
   |
13 | #[derive(Clone, Debug, Parser)]
   |                        ^^^^^^ implementation of `FnOnce` is not general enough
   |
   = note: `fn(&'2 str) -> Result<Offset, anyhow::Error> {Offset::parse::<&'2 str>}` must implement `FnOnce<(&'1 str,)>`, for any lifetime `'1`...
   = note: ...but it actually implements `FnOnce<(&'2 str,)>`, for some specific lifetime `'2`
   = note: this error originates in the derive macro `Parser` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0308]: mismatched types
   --> src/main.rs:13:24
    |
13  | #[derive(Clone, Debug, Parser)]
    |                        ^^^^^^ one type is more general than the other
    |
    = note: expected trait `for<'a> Fn<(&'a str,)>`
               found trait `Fn<(&str,)>`
note: the lifetime requirement is introduced here
   --> /Users/luke/.cargo/registry/src/github.com-1ecc6299db9ec823/clap_builder-4.3.0/src/builder/arg.rs:975:48
    |
975 |     pub fn value_parser(mut self, parser: impl IntoResettable<super::ValueParser>) -> Self {
    |                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    = note: this error originates in the derive macro `Parser` (in Nightly builds, run with -Z macro-backtrace for more info)

Expected Behaviour

Successful compilation.

Additional Context

This program compiled successfully with clap 4.2, and broke in clap 4.3.

Debug Output

Same as "actual behavior" above.

lukesneeringer avatar May 24 '23 19:05 lukesneeringer

This program compiled successfully with clap 4.2, and broke in clap 4.3.

I just ran with

#!/usr/bin/env rust-script

//! ```cargo
//! [dependencies]
//! clap = { version = "=4.2.0", features = ["derive", "debug"] }
//! anyhow = "1"
//! ```

use anyhow::Result;
use clap::Parser;

#[derive(Clone, Debug)]
struct Offset {}

impl Offset {
    pub fn parse(s: impl AsRef<str>) -> Result<Self> {
        Ok(Self {})
    }
}

#[derive(Clone, Debug, Parser)]
struct Command {
    #[arg(long, value_parser = Offset::parse)]
    pub offset: Offset,
}

fn main() {
    println!("Hello, world!");
}

and I'm seeing the failure

even if I drop to

#!/usr/bin/env rust-script

//! ```cargo
//! [dependencies]
//! clap = { version = "=4.0.0", features = ["derive", "debug"] }
//! anyhow = "1"
//! ```

use anyhow::Result;
use clap::Parser;

#[derive(Clone, Debug)]
struct Offset {}

impl Offset {
    pub fn parse(s: impl AsRef<str>) -> Result<Self> {
        Ok(Self {})
    }
}

#[derive(Clone, Debug, Parser)]
struct Command {
    #[arg(long, value_parser = Offset::parse)]
    pub offset: Offset,
}

fn main() {
    println!("Hello, world!");
}

I see the failure.

epage avatar May 24 '23 20:05 epage

Rust Version: 1.74.1 Clap Version: 4.4.11

@epage @lukesneeringer I did a bit of diving into the the problem since I ran into something similar on latest clap & latest rust. I made the smallest reproducible example I could:

Problem

#[derive(Clone)]
struct Offset;

pub fn offset_parse(_: impl AsRef<str>) -> Result<Offset, String> {
    Ok(Offset)
}

fn main() {
    let arg = clapArg::new("offset").value_parser(offset_parse);
    let _cmd = clap::Command::new("command").arg(arg);
}

the compiler (nightly or stable) still seems to have a hard time at showing the actual origin of the problem when dealing with higher ranked trait bounds, the actual source of the error is the line below: https://github.com/clap-rs/clap/blob/d092896d61fd73a5467db85eac035a9ce2ddbc60/clap_builder/src/builder/value_parser.rs#L910

Partial solution

I found a reddit comment that touches very closely on the problem encountered in this issue and managed to reproduce most of the pattern: https://old.reddit.com/r/rust/comments/f1spyt/higher_kinded_lifetime_on_return_types/fh8n2b2/

However, the last step in the post using the nested function that returns an impl still seems to be tricky to invoke:

As it turns out, rustc is quite bad with type inference when it comes to HRTB - for some weird reason here it cannot > prove that our closure is general enough, even though it is. Well, once again, we have to help the compiler:

fn fancy_copy_2(value: i32) -> i32 {
    fn build_callback(value: i32) -> impl FnOnce(&Source) -> Ref {
        move |s| {
            s.new_ref(value)
        }
    }

    call_2(build_callback(value))
}

I've made a separate feature branch to test out the issue that includes the modified impl<F, T, E> TypedValueParser for F: https://github.com/clap-rs/clap/blob/9920bfaaa7b1488f7106d906c68dfbac74580063/clap_builder/src/builder/value_parser.rs#L908-L935

As well as a unit test to reproduce the issue: https://github.com/clap-rs/clap/blob/9920bfaaa7b1488f7106d906c68dfbac74580063/clap_builder/src/builder/tests.rs#L58-L69

There is still a compilation error but I do believe this is closer to being compiled:

$ cargo test

error: implementation of `Callback` is not general enough
  --> clap_builder/src/builder/tests.rs:67:15
   |
67 |     let arg = Arg::new("offset").value_parser(offset_parse);
   |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `Callback` is not general enough
   |
   = note: `fn(&str) -> std::result::Result<Offset, String> {offset_parse::<&str>}` must implement `Callback<'0>`, for any lifetime `'0`...
   = note: ...but it actually implements `Callback<'1>`, for some specific lifetime `'1`

mkatychev avatar Dec 16 '23 23:12 mkatychev

I've experimented with this for a while now and I think this is some limitation in the compiler - I've reported it here, we'll see: https://github.com/rust-lang/rust/issues/119045

I think the best solution for now is to use a closure, i.e.:

let arg = Arg::new("offset").value_parser(|a: &str| offset_parse(a));

Patryk27 avatar Dec 17 '23 12:12 Patryk27

EDIT: rust: rustc 1.80.0 (051478957 2024-07-21) clap: 4.5.13

I encountered the similar problem. I'm using clap with builder pattern. Minimal steps to reproduce:

#[derive(Clone)]
pub enum ListColumn {
    All,
    Url,
    Tag,
    Desc,
}

// make command and this as arg
                .arg(
                    Arg::new("cols")
                        .short('c')
                        .long("cols")
                        .default_value("all")
                        .value_parser(|str| {
                            return if str == "all" {
                                Ok(ListColumn::All)
                            } else if str == "tags" {
                                Ok(ListColumn::Tag)
                            } else {
                                Err(clap::Error::new(clap::error::ErrorKind::InvalidValue))
                            };
                        })
                        .help("List the specified cols")
                )

// after getting Argmatches
    match matches.subcommand() {
    // ...
        Some(("list", list_task)) => {
                let column = list_task.get_one::<ListColumn>("cols").unwrap();
                // ...
        }
        _ => {}
    }

And I encountered the error like this:

rustc: implementation of `FnOnce` is not general enough
closure with signature `fn(&'2 str) -> Result<ListColumn, clap::error::Error>` must implement `FnOnce<(&'1 str,)>`, for any lifetime `'1`...
...but it actually implements `FnOnce<(&'2 str,)>`, for some specific lifetime `'2`
rustc: implementation of `Fn` is not general enough
closure with signature `fn(&'2 str) -> Result<ListColumn, clap::error::Error>` must implement `Fn<(&'1 str,)>`, for any lifetime `'1`...
...but it actually implements `Fn<(&'2 str,)>`, for some specific lifetime `'2`

What I'm trying to do is to restrict the user to provide the value only supported by enum ListColumn and thus in value_parser() I've written a small sample logic to accept or throw error based on what is provided by user.

Is there some better way to do this ? Or what I'm doing is correct and this is like a genuine error which needs to be fixed ?

abhaysp95 avatar Aug 17 '24 10:08 abhaysp95

@abhaysp95 I expect a workaround would be to split the closure out into a dedicated function.

Also, using clap::Error might not have the best results. Maybe thats something we can dynamic cast for an improve.

You could also do

PossibleValuesParser::new(["all", "tags"]).map(|s| {
    match s {
        "all" => ListColumn::All,
        "tags" => ListColumn::AllTag,
        _ => unimplemented!("PossibleValuesParser prevents getting there")
    }
})

epage avatar Aug 19 '24 14:08 epage