synek317
synek317

Reputation: 819

Can a function accept an argument of type Option or &str or String?

I'm trying to create a fast, flexible and convenient API that accepts an optional string parameter. I wish the user to be able to pass:

As far as I know, the best solution to handle "foo" or "foo".to_string() is Into<Cow<'a, str>>. On the other hand, the best solution to handle "foo" or Some("foo") is Into<Option<&'a str>>.

Thus I tried with this but it doesn't compile:

fn foo<'a, T, O>(_bar: O)
where
    T: Into<Cow<'a, str>>,
    O: Into<Option<T>>,
foo(Some("aaa"));
error[E0283]: type annotations required: cannot resolve `_: std::convert::Into<std::borrow::Cow<'_, str>>`
  --> src/main.rs:12:5
   |
12 |     foo(Some("aaa"));
   |     ^^^
   |
note: required by `foo`
  --> src/main.rs:3:1
   |
3  | / fn foo<'a, T, O>(_bar: O)
4  | | where
5  | |     T: Into<Cow<'a, str>>,
6  | |     O: Into<Option<T>>,
7  | | {}
   | |__^

Playground

Is it possible to make it work?

Upvotes: 5

Views: 1958

Answers (1)

Shepmaster
Shepmaster

Reputation: 430290

I'm pretty sure you cannot create a function like this and still have it be ergonomically used. The problem is that there can be zero, one, or multiple potential paths through the generic types:

            +-----------+
            |           |
  +---------> Option<B> +----------------------+
  |         |           |                      |
+-+-+       +-----------+          +-----------v----------+
|   |                              |                      |
| A |                              | Option<Cow<'a, str>> |
|   |                              |                      |
+-+-+       +-----------+          +-----------^----------+
  |         |           |                      |
  +---------> Option<C> +----------------------+
            |           |
            +-----------+

That's why you are getting the error you are: It's unclear what the concrete type of T should be, thus the caller would have to provide it to the compiler. Here I use the turbofish:

foo::<&str, _>(Some("aaa"));
foo::<String, _>(Some("aaa".to_string()));
foo::<&str, Option<&str>>(None);

I'd suggest re-evaluating your API design. Possible directions include:

  1. Creating a custom struct and implementing From for specific concrete types (e.g. &str, Option<String>, etc.). Passing None will still have the problem because it's unclear what type of None it is: an Option<&str> or Option<String>?

    use std::borrow::Cow;
    
    fn foo<'a, C>(_bar: C)
    where
        C: Into<Config<'a>>,
    {
    }
    
    struct Config<'a>(Option<Cow<'a, str>>);
    
    impl<'a> From<&'a str> for Config<'a> {
        fn from(other: &'a str) -> Config<'a> {
            Config(Some(other.into()))
        }
    }
    
    impl From<String> for Config<'static> {
        fn from(other: String) -> Config<'static> {
            Config(Some(other.into()))
        }
    }
    
    impl<'a> From<Option<&'a str>> for Config<'a> {
        fn from(other: Option<&'a str>) -> Config<'a> {
            Config(other.map(Into::into))
        }
    }
    
    impl From<Option<String>> for Config<'static> {
        fn from(other: Option<String>) -> Config<'static> {
            Config(other.map(Into::into))
        }
    }
    
    fn main() {
        foo("aaa");
        foo("aaa".to_string());
    
        foo(Some("aaa"));
        foo(Some("aaa".to_string()));
        foo(None::<&str>);
    }
    
  2. Switch to a builder pattern — my preferred direction:

    use std::borrow::Cow;
    
    #[derive(Debug, Clone, Default)]
    struct Foo<'a> {
        name: Option<Cow<'a, str>>,
    }
    
    impl<'a> Foo<'a> {
        fn new() -> Self {
            Self::default()
        }
    
        fn name<S>(mut self, name: S) -> Self
        where
            S: Into<Cow<'a, str>>,
        {
            self.name = Some(name.into());
            self
        }
    
        fn go(self) {
            println!("The name is {:?}", self.name)
        }
    }
    
    fn main() {
        Foo::new().go();
        Foo::new().name("aaa").go();
        Foo::new().name("aaa".to_string()).go();
    }
    

    Note that this removes the need for the caller to specify Some at all; using the name function implies presence. You could make a without_name function to set it back to None if needed.

See also:

Upvotes: 8

Related Questions