简体   繁体   中英

How can I use a function as a closure when the function needs to take a reference to the closures argument?

The following code works ( cargo +nightly run ) fine:

fn main() {
    let res: Result<(), String> = Err(String::from("hi"));
    println!("{}", res.map_err(shout).unwrap_err())
}

fn shout(s: String) -> String {
    s.to_ascii_uppercase()
}

Clippy ( cargo +nightly clippy ) spits out a (justified) warning:

warning: this argument is passed by value, but not consumed in the function body
 --> src/main.rs:6:13
  |
6 | fn shout(s: String) -> String {
  |             ^^^^^^ help: consider changing the type to: `&str`

Changing the code to the suggested version

fn shout(s: &str) -> String {
    s.to_ascii_uppercase()
}

results in a compiler error:

error[E0631]: type mismatch in function arguments
 --> src/main.rs:3:24
  |
3 |     println!("{}", res.map_err(shout).unwrap_err())
  |                        ^^^^^^^ expected signature of `fn(std::string::String) -> _`
...
6 | fn shout(s: &str) -> String {
  | --------------------------- found signature of `for<'r> fn(&'r str) -> _`

What is the right way to react? Sure, I could simply do #![cfg_attr(feature="clippy", allow(needless_pass_by_value))] but this feels wrong to me. Is there a way to use map_err with the version of shout taking a reference?

The best you can do is use a full closure:

res.map_err(|x| shout(&x)).unwrap_err()

Your original form has two steps needed to be able to work:

  1. It needs to take the argument to the closure and convert it to a reference.
  2. It needs to convert the &String to a &str .

Additionally, it needs to do both of those while the value is in scope, so that it doesn't end up with a dangling reference. Neither of these are things the "short" form of closures handles right now — the types must match exactly.

If you really wanted to avoid the closure, you can for this specific case:

res.as_ref().map_err(String::as_str).map_err(shout).unwrap_err()
//  ^~~~~~           ^~~~~~~~~~~~~~
//  |                |
//  |                |- Convert `&String` to `&str`
//  |   
//  |- Get a reference (`&String`)   

I actually argued for the ability for your original code to work as part of the ergonomics initiative, but it did not seem to gain traction.


Like many problems in programming, you can "solve" this by adding more abstraction. Here, we introduce a trait to embody the concept of "an error that can be shouted":

fn main() {
    let e1 = Err::<(), _>(String::from("hi"));
    println!("{}", e1.map_err(ShoutyError::shout).unwrap_err());

    let e2 = Err::<(), _>(42);
    println!("{}", e2.map_err(ShoutyError::shout).unwrap_err());
}

trait ShoutyError {
    fn shout(self) -> String;
}

impl ShoutyError for String {
    fn shout(self) -> String {
        self.to_ascii_uppercase()
    }
}

impl ShoutyError for i32 {
    fn shout(self) -> String {
        format!("I YELL {}", self)
    }
}

If you felt like you needed it, you could also have a wrapper function to keep the exact initial code:

fn shout<E: ShoutyError>(e: E) -> String {
    e.shout()
}

I'd like to have a function adapt that takes one function f : &T -> U and returns a new function g : T -> U .

This is possible, but only in nightly Rust:

#![feature(conservative_impl_trait)]

fn adapt<F, T, U>(f: F) -> impl Fn(T) -> U
where
    F: Fn(&T) -> U,
{
    move |arg| f(&arg)
}

Unfortunately, it doesn't solve your problem because shout doesn't accept a &String and this would require str to be a Sized type.

The more verbose solution involves AsRef :

#![feature(conservative_impl_trait)]

fn adapt<F, T1, T2, U>(f: F) -> impl Fn(T1) -> U
where
    F: Fn(&T2) -> U,
    T1: AsRef<T2>,
    T2: ?Sized,
{
    move |arg| f(arg.as_ref())
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM