How to avoid Type annotations on an Into<Option<Into<X>>>?

Hi,

A simple one. Please, how do we accept None on this double-Into parameter?

pub fn new<P: Into<Precision>>(prec: impl Into<Option<P>>) -> Self {
    Self(prec.into().map(Into::into))
}

I have a From<u8> impl block for Precision, and I want to be able to call new(5) or new(Precision::Full) or new(None), but the latter fails with Type annotation needed. It should be obvious that it is a None::<Precision>, shouldn't it? :thinking:

Playground

Thank you.

No, why? The whole point of P: Into<Precision> is that it can be any type that implements Into<Precision>.


As-is, you can #[derive(Default)] on Data, and then Data::default() will give you Data(None).

Thanks.
Because of the target field, I'm setting an Option<Precision> after all.
My actual struct does implement Default, but I didn't quite get your suggestion.
I need to make the None work, and currently, only None::<Precision> does.

Yes, but why should the compiler prefer one particular Into<Precision> impl over another? It's not sentient. One impl is just as good as another, even if one of them is reflexive.

this

1 Like

Ah, I see, but I really wanted a simple None there.
I was expecting some trick like this one: Function to accept T or Option<T: impl Trait> - #5 by SDX, but I'm not sure whether my case is the same or not.

my advice is to generally avoid impl Into paramaters, and let users call .into() themselves. having a concrete argument type gives more information to the type inference algorithm, and also empowers deref coercion.

it's a lot easier to make a good non-generic api than a good generic api.

one frequent pain point is functions doing trickery in order to accept String or &str, but if i give them &String (which would usually coerce to &str), it doesn't know what to do, and i have to create a seperate let binding with a type annotation.

4 Likes

Thanks binarycat, but I love them! I think they grant a richer API.
But I think this is the first time I've tried it twice. This is my actual constructor at the moment:

    pub fn new<'a, P: Into<Precision>, S: Into<Separator>>(
        unit: impl Into<Cow<'a, str>>,
        sys: impl Into<Option<System>>,
        prec: impl Into<Option<P>>,
        sep: impl Into<Option<S>>,
    ) -> Self {
        Self {
            unit: utils::intern(unit.into()),
            sys: sys.into(),
            prec: prec.into().map(Into::into),
            sep: sep.into().map(Into::into),
        }
    }

So, in fact, prec is:

impl Into<Option<impl Into<Precision>>>

which allows me to send either 5, Precision::<variant>, or None.

Thinking this through, if I send 5, it seems the correct would be to:

  1. call Precision::from(5) to get a Precision
  2. call Option::from(Precision) to get the final Option<Precision>

But, since the type starts with impl Into<Option<..., Rust wants me to call it prec.into().map(Into::into), i.e. first convert to an Option<unknown>, and then call within its content unknown.into(), and I think that's what breaks it.
So, would there be some way of first calling 1 then 2?

One thing you can do here is define a custom trait.

pub struct Precision;

pub trait IntoOptionalPrecision {
    fn into_optional_precision(self) -> Option<Precision>;
}

impl IntoOptionalPrecision for () {
    fn into_optional_precision(self) -> Option<Precision> {
        None
    }
}

impl<T: Into<Precision>> IntoOptionalPrecision for Option<T> {
    fn into_optional_precision(self) -> Option<Precision> {
        self.map(Into::into)
    }
}

impl<T: Into<Precision>> IntoOptionalPrecision for T {
    fn into_optional_precision(self) -> Option<Precision> {
        Some(self.into())
    }
}

This way, all the input cases you want are supported, and you can express always-None as (), which is unambiguous since it doesn't have a type parameter like Option does.

Custom conversion traits like this are not uncommon in libraries that want to offer very flexible inputs — but they are usually about types that are a big deal in that library.

4 Likes

yes, this is what reqwest does with IntoUrl. the fact that there are so many options for what to use as the conversion trait is part of what makes designing this api so tricky.

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.